diff --git a/README.rst b/README.rst
index 6c6ab16..545ff88 100644
--- a/README.rst
+++ b/README.rst
@@ -50,6 +50,8 @@ And enable it in Horizon::
cp ../zun-ui/zun_ui/enabled/_1330_project_container_panelgroup.py openstack_dashboard/local/enabled
cp ../zun-ui/zun_ui/enabled/_1331_project_container_containers_panel.py openstack_dashboard/local/enabled
+ cp ../zun-ui/zun_ui/enabled/_2330_project_container_panelgroup.py openstack_dashboard/local/enabled
+ cp ../zun-ui/zun_ui/enabled/_2331_project_container_images_panel.py openstack_dashboard/local/enabled
To run horizon with the newly enabled Zun UI plugin run::
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 71c7aaf..84d4f60 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -50,6 +50,8 @@ And enable it in Horizon::
cp ../zun-ui/zun_ui/enabled/_1330_project_container_panelgroup.py openstack_dashboard/local/enabled
cp ../zun-ui/zun_ui/enabled/_1331_project_container_containers_panel.py openstack_dashboard/local/enabled
+ cp ../zun-ui/zun_ui/enabled/_2330_project_container_panelgroup.py openstack_dashboard/local/enabled
+ cp ../zun-ui/zun_ui/enabled/_2331_project_container_images_panel.py openstack_dashboard/local/enabled
To run horizon with the newly enabled Zun UI plugin run::
diff --git a/zun_ui/api/client.py b/zun_ui/api/client.py
index 3ec3a08..a416c94 100644
--- a/zun_ui/api/client.py
+++ b/zun_ui/api/client.py
@@ -21,6 +21,7 @@ from zunclient.v1 import client as zun_client
LOG = logging.getLogger(__name__)
CONTAINER_CREATE_ATTRS = zun_client.containers.CREATION_ATTRIBUTES
+IMAGE_PULL_ATTRS = zun_client.images.PULL_ATTRIBUTES
@memoized
@@ -133,3 +134,22 @@ def container_kill(request, id, signal=None):
def container_attach(request, id):
return zunclient(request).containers.attach(id)
+
+
+def image_list(request, limit=None, marker=None, sort_key=None,
+ sort_dir=None, detail=True):
+ return zunclient(request).images.list(limit, marker, sort_key,
+ sort_dir, False)
+
+
+def image_create(request, **kwargs):
+ args = {}
+ for (key, value) in kwargs.items():
+
+ if key in IMAGE_PULL_ATTRS:
+ args[str(key)] = str(value)
+ else:
+ raise exceptions.BadRequest(
+ "Key must be in %s" % ",".join(IMAGE_PULL_ATTRS))
+
+ return zunclient(request).images.create(**args)
diff --git a/zun_ui/api/rest_api.py b/zun_ui/api/rest_api.py
index e5fb1d8..0421c80 100644
--- a/zun_ui/api/rest_api.py
+++ b/zun_ui/api/rest_api.py
@@ -118,3 +118,30 @@ class Containers(generic.View):
return rest_utils.CreatedResponse(
'/api/zun/container/%s' % new_container.uuid,
new_container.to_dict())
+
+
+@urls.register
+class Images(generic.View):
+ """API for Zun Images"""
+ url_regex = r'zun/images/$'
+
+ @rest_utils.ajax()
+ def get(self, request):
+ """Get a list of the Images for admin users.
+
+ The returned result is an object with property 'items' and each
+ item under this is a Image.
+ """
+ result = client.image_list(request)
+ return {'items': [change_to_id(i.to_dict()) for i in result]}
+
+ @rest_utils.ajax(data_required=True)
+ def post(self, request):
+ """Create a new Image.
+
+ Returns the new Image object on success.
+ """
+ new_image = client.image_create(request, **request.DATA)
+ return rest_utils.CreatedResponse(
+ '/api/zun/image/%s' % new_image.uuid,
+ new_image.to_dict())
diff --git a/zun_ui/content/container/images/__init__.py b/zun_ui/content/container/images/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/zun_ui/content/container/images/panel.py b/zun_ui/content/container/images/panel.py
new file mode 100644
index 0000000..c62a573
--- /dev/null
+++ b/zun_ui/content/container/images/panel.py
@@ -0,0 +1,19 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+import horizon
+
+
+class Images(horizon.Panel):
+ name = _("Images")
+ slug = "container.images"
diff --git a/zun_ui/content/container/images/urls.py b/zun_ui/content/container/images/urls.py
new file mode 100644
index 0000000..2620e7f
--- /dev/null
+++ b/zun_ui/content/container/images/urls.py
@@ -0,0 +1,20 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls import url
+from django.utils.translation import ugettext_lazy as _
+from horizon.browsers import views
+
+title = _("Images")
+urlpatterns = [
+ url('', views.AngularIndexView.as_view(title=title), name='index'),
+]
diff --git a/zun_ui/enabled/_2330_admin_container_panelgroup.py b/zun_ui/enabled/_2330_admin_container_panelgroup.py
new file mode 100644
index 0000000..a871611
--- /dev/null
+++ b/zun_ui/enabled/_2330_admin_container_panelgroup.py
@@ -0,0 +1,20 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+# The slug of the panel group to be added to HORIZON_CONFIG. Required.
+PANEL_GROUP = 'container'
+# The display name of the PANEL_GROUP. Required.
+PANEL_GROUP_NAME = _('Container')
+# The slug of the dashboard the PANEL_GROUP associated with. Required.
+PANEL_GROUP_DASHBOARD = 'admin'
diff --git a/zun_ui/enabled/_2331_admin_container_images_panel.py b/zun_ui/enabled/_2331_admin_container_images_panel.py
new file mode 100644
index 0000000..a18f1f8
--- /dev/null
+++ b/zun_ui/enabled/_2331_admin_container_images_panel.py
@@ -0,0 +1,21 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# The slug of the panel to be added to HORIZON_CONFIG. Required.
+PANEL = 'container.images'
+# The slug of the panel group the PANEL is associated with.
+PANEL_GROUP = 'container'
+# The slug of the dashboard the PANEL associated with. Required.
+PANEL_DASHBOARD = 'admin'
+
+# Python panel class of the PANEL to be added.
+ADD_PANEL = 'zun_ui.content.container.images.panel.Images'
diff --git a/zun_ui/static/dashboard/container/container.module.js b/zun_ui/static/dashboard/container/container.module.js
index eb8a504..d6b1e30 100644
--- a/zun_ui/static/dashboard/container/container.module.js
+++ b/zun_ui/static/dashboard/container/container.module.js
@@ -24,6 +24,7 @@
angular
.module('horizon.dashboard.container', [
'horizon.dashboard.container.containers',
+ 'horizon.dashboard.container.images',
'ngRoute'
])
.config(config);
diff --git a/zun_ui/static/dashboard/container/images/actions.module.js b/zun_ui/static/dashboard/container/images/actions.module.js
new file mode 100644
index 0000000..061c23f
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/actions.module.js
@@ -0,0 +1,58 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc overview
+ * @ngname horizon.dashboard.container.images.actions
+ *
+ * @description
+ * Provides all of the actions for images.
+ */
+ angular.module('horizon.dashboard.container.images.actions',
+ [
+ 'horizon.framework',
+ 'horizon.dashboard.container'
+ ])
+ .run(registerImageActions);
+
+ registerImageActions.$inject = [
+ 'horizon.framework.conf.resource-type-registry.service',
+ 'horizon.framework.util.i18n.gettext',
+ 'horizon.dashboard.container.images.actions.create.service',
+ 'horizon.dashboard.container.images.resourceType'
+ ];
+
+ function registerImageActions(
+ registry,
+ gettext,
+ createImageService,
+ resourceType
+ ) {
+ var imagesResourceType = registry.getResourceType(resourceType);
+
+ imagesResourceType.globalActions
+ .append({
+ id: 'createImageAction',
+ service: createImageService,
+ template: {
+ type: 'create',
+ text: gettext('Pull Image')
+ }
+ });
+ }
+
+})();
diff --git a/zun_ui/static/dashboard/container/images/actions/create.service.js b/zun_ui/static/dashboard/container/images/actions/create.service.js
new file mode 100644
index 0000000..c00637e
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/actions/create.service.js
@@ -0,0 +1,84 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc factory
+ * @name horizon.dashboard.container.images.create.service
+ * @description
+ * Service for the pull image modal
+ */
+ angular
+ .module('horizon.dashboard.container.images.actions')
+ .factory('horizon.dashboard.container.images.actions.create.service', createImageService);
+
+ createImageService.$inject = [
+ 'horizon.app.core.openstack-service-api.policy',
+ 'horizon.app.core.openstack-service-api.zun',
+ 'horizon.dashboard.container.images.actions.workflow',
+ 'horizon.dashboard.container.images.resourceType',
+ 'horizon.framework.util.actions.action-result.service',
+ 'horizon.framework.util.i18n.gettext',
+ 'horizon.framework.util.q.extensions',
+ 'horizon.framework.widgets.form.ModalFormService',
+ 'horizon.framework.widgets.toast.service'
+ ];
+
+ function createImageService(
+ policy, zun, workflow, resourceType,
+ actionResult, gettext, $qExtensions, modal, toast
+ ) {
+
+ var message = {
+ success: gettext('Image %s was successfully pulled.')
+ };
+
+ var service = {
+ initAction: initAction,
+ perform: perform,
+ allowed: allowed
+ };
+
+ return service;
+
+ //////////////
+
+ function initAction() {
+ }
+
+ function perform() {
+ var title, submitText;
+ title = gettext('Pull Image');
+ submitText = gettext('Pull');
+ var config = workflow.init('create', title, submitText);
+ return modal.open(config).then(submit);
+ }
+
+ function allowed() {
+ return policy.ifAllowed({ rules: [['image', 'pull_image']] });
+ }
+
+ function submit(context) {
+ return zun.pullImage(context.model, true).then(success, true);
+ }
+
+ function success(response) {
+ toast.add('success', interpolate(message.success, [response.data.id]));
+ var result = actionResult.getActionResult().created(resourceType, response.data.name);
+ return result.result;
+ }
+ }
+})();
diff --git a/zun_ui/static/dashboard/container/images/actions/workflow.service.js b/zun_ui/static/dashboard/container/images/actions/workflow.service.js
new file mode 100644
index 0000000..d497884
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/actions/workflow.service.js
@@ -0,0 +1,88 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc factory
+ * @name horizon.dashboard.container.images.workflow
+ * @description
+ * Workflow for pulling image
+ */
+ angular
+ .module('horizon.dashboard.container.images.actions')
+ .factory('horizon.dashboard.container.images.actions.workflow', workflow);
+
+ workflow.$inject = [
+ 'horizon.framework.util.i18n.gettext'
+ ];
+
+ function workflow(gettext) {
+ var workflow = {
+ init: init
+ };
+
+ function init(actionType, title, submitText) {
+ var schema, form, model;
+
+ // schema
+ schema = {
+ type: 'object',
+ properties: {
+ repo: {
+ title: gettext('Image'),
+ type: 'string'
+ }
+ }
+ };
+
+ // form
+ form = [
+ {
+ type: 'section',
+ htmlClass: 'row',
+ items: [
+ {
+ type: 'section',
+ htmlClass: 'col-sm-12',
+ items: [
+ {
+ key: 'repo',
+ placeholder: gettext('Name of the image.')
+ }
+ ]
+ }
+ ]
+ }
+ ]; // form
+
+ model = {
+ repo: ''
+ };
+
+ var config = {
+ title: title,
+ submitText: submitText,
+ schema: schema,
+ form: form,
+ model: model
+ };
+
+ return config;
+ }
+
+ return workflow;
+ }
+})();
diff --git a/zun_ui/static/dashboard/container/images/drawer.html b/zun_ui/static/dashboard/container/images/drawer.html
new file mode 100644
index 0000000..812e6d9
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/drawer.html
@@ -0,0 +1,5 @@
+
+
diff --git a/zun_ui/static/dashboard/container/images/images.module.js b/zun_ui/static/dashboard/container/images/images.module.js
new file mode 100644
index 0000000..73fa7d1
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/images.module.js
@@ -0,0 +1,142 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc overview
+ * @name horizon.dashboard.container.images
+ * @ngModule
+ * @description
+ * Provides all the services and widgets require to display the images
+ * panel
+ */
+ angular
+ .module('horizon.dashboard.container.images', [
+ 'ngRoute',
+ 'horizon.dashboard.container.images.actions'
+ ])
+ .constant('horizon.dashboard.container.images.events', events())
+ .constant('horizon.dashboard.container.images.resourceType', 'OS::Zun::Image')
+ .run(run)
+ .config(config);
+
+ /**
+ * @ngdoc constant
+ * @name horizon.dashboard.container.images.events
+ * @description A list of events used by Images
+ * @returns {Object} Event constants
+ */
+ function events() {
+ return {
+ CREATE_SUCCESS: 'horizon.dashboard.container.images.CREATE_SUCCESS',
+ DELETE_SUCCESS: 'horizon.dashboard.container.images.DELETE_SUCCESS'
+ };
+ }
+
+ run.$inject = [
+ 'horizon.framework.conf.resource-type-registry.service',
+ 'horizon.app.core.openstack-service-api.zun',
+ 'horizon.dashboard.container.images.basePath',
+ 'horizon.dashboard.container.images.resourceType',
+ 'horizon.dashboard.container.images.service'
+ ];
+
+ function run(registry, zun, basePath, resourceType, imageService) {
+ registry.getResourceType(resourceType)
+ .setNames(gettext('Image'), gettext('Images'))
+ // for detail summary view on table row.
+ .setSummaryTemplateUrl(basePath + 'drawer.html')
+ // for table row items and detail summary view.
+ .setProperties(imageProperties())
+ .setListFunction(imageService.getImagesPromise)
+ .tableColumns
+ .append({
+ id: 'id',
+ priority: 2
+ })
+ .append({
+ id: 'repo',
+ priority: 1,
+ sortDefault: true
+ })
+ .append({
+ id: 'tag',
+ priority: 1
+ })
+ .append({
+ id: 'size',
+ priority: 1
+ })
+ .append({
+ id: 'image_id',
+ priority: 3
+ });
+ // for magic-search
+ registry.getResourceType(resourceType).filterFacets
+ .append({
+ 'label': gettext('Image'),
+ 'name': 'repo',
+ 'singleton': true
+ })
+ .append({
+ 'label': gettext('Tag'),
+ 'name': 'tag',
+ 'singleton': true
+ })
+ .append({
+ 'label': gettext('ID'),
+ 'name': 'id',
+ 'singleton': true
+ })
+ .append({
+ 'label': gettext('Image ID'),
+ 'name': 'image_id',
+ 'singleton': true
+ });
+ }
+
+ function imageProperties() {
+ return {
+ 'id': {label: gettext('ID'), filters: ['noValue'] },
+ 'repo': { label: gettext('Image'), filters: ['noValue'] },
+ 'tag': { label: gettext('Tag'), filters: ['noValue'] },
+ 'size': { label: gettext('Size'), filters: ['noValue', 'bytes'] },
+ 'image_id': { label: gettext('Image ID'), filters: ['noValue'] }
+ };
+ }
+
+ config.$inject = [
+ '$provide',
+ '$windowProvider',
+ '$routeProvider'
+ ];
+
+ /**
+ * @name config
+ * @param {Object} $provide
+ * @param {Object} $windowProvider
+ * @param {Object} $routeProvider
+ * @description Routes used by this module.
+ * @returns {undefined} Returns nothing
+ */
+ function config($provide, $windowProvider, $routeProvider) {
+ var path = $windowProvider.$get().STATIC_URL + 'dashboard/container/images/';
+ $provide.constant('horizon.dashboard.container.images.basePath', path);
+ $routeProvider.when('/admin/container/images', {
+ templateUrl: path + 'panel.html'
+ });
+ }
+})();
diff --git a/zun_ui/static/dashboard/container/images/images.service.js b/zun_ui/static/dashboard/container/images/images.service.js
new file mode 100644
index 0000000..97f35f8
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/images.service.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+ "use strict";
+
+ angular
+ .module('horizon.dashboard.container.images')
+ .factory('horizon.dashboard.container.images.service', imagesService);
+
+ imagesService.$inject = [
+ 'horizon.app.core.detailRoute',
+ 'horizon.app.core.openstack-service-api.zun'
+ ];
+
+ /*
+ * @ngdoc factory
+ * @name horizon.dashboard.container.images.service
+ *
+ * @description
+ * This service provides functions that are used through
+ * the images of container features.
+ */
+ function imagesService(detailRoute, zun) {
+ return {
+ getImagesPromise: getImagesPromise
+ };
+
+ /*
+ * @ngdoc function
+ * @name getImagesPromise
+ * @description
+ * Given filter/query parameters, returns a promise for the matching
+ * images. This is used in displaying lists of images.
+ */
+ function getImagesPromise(params) {
+ return zun.getImages(params).then(modifyResponse);
+ }
+
+ function modifyResponse(response) {
+ return {data: {items: response.data.items.map(modifyItem)}};
+
+ function modifyItem(item) {
+ var timestamp = new Date();
+ item.trackBy = item.id.concat(timestamp.getTime());
+ return item;
+ }
+ }
+ }
+})();
diff --git a/zun_ui/static/dashboard/container/images/panel.html b/zun_ui/static/dashboard/container/images/panel.html
new file mode 100644
index 0000000..40dfdd0
--- /dev/null
+++ b/zun_ui/static/dashboard/container/images/panel.html
@@ -0,0 +1,4 @@
+
+
+
diff --git a/zun_ui/static/dashboard/container/zun.service.js b/zun_ui/static/dashboard/container/zun.service.js
index 277febf..9754bc1 100644
--- a/zun_ui/static/dashboard/container/zun.service.js
+++ b/zun_ui/static/dashboard/container/zun.service.js
@@ -26,6 +26,7 @@
function ZunAPI(apiService, toast, gettext) {
var containersPath = '/api/zun/containers/';
+ var imagesPath = '/api/zun/images/';
var service = {
createContainer: createContainer,
getContainer: getContainer,
@@ -40,7 +41,9 @@
pauseContainer: pauseContainer,
unpauseContainer: unpauseContainer,
executeContainer: executeContainer,
- killContainer: killContainer
+ killContainer: killContainer,
+ pullImage: pullImage,
+ getImages: getImages
};
return service;
@@ -126,6 +129,20 @@
return apiService.post(containersPath + id + '/kill', params).error(error(msg));
}
+ ////////////
+ // Images //
+ ////////////
+
+ function pullImage(params) {
+ var msg = gettext('Unable to pull Image.');
+ return apiService.post(imagesPath, params).error(error(msg));
+ }
+
+ function getImages() {
+ var msg = gettext('Unable to retrieve the Images.');
+ return apiService.get(imagesPath).error(error(msg));
+ }
+
function error(message) {
return function() {
toast.add('error', message);