diff --git a/app/scripts/app.js b/app/scripts/app.js index 3c63d73dfb..a0456a88fb 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -438,6 +438,14 @@ angular // as a sanity test and shouldn't block submitting the form. Rely on the API // server for any additional validation. .constant('SOURCE_URL_PATTERN', /^[a-z][a-z0-9+.-@]*:(\/\/)?[0-9a-z_-]+/i) + // RELATIVE_PATH_PATTERN matches any paths not starting with `/` or + // containing `..` as path elements. Use negative lookaheads to assert that + // the value does not match those patterns. + // + // (?!\/) do not match strings starting with `/` + // (?!\.\.(\/|$)) do not match strings starting with `../` or exactly `..` + // (?!.*\/\.\.(\/|$)) do not match strings containing `/../` or ending in `/..` + .constant('RELATIVE_PATH_PATTERN', /^(?!\/)(?!\.\.(\/|$))(?!.*\/\.\.(\/|$)).*$/) // http://stackoverflow.com/questions/9038625/detect-if-device-is-ios .constant('IS_IOS', /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) .constant('amTimeAgoConfig', { diff --git a/app/scripts/controllers/addConfigVolume.js b/app/scripts/controllers/addConfigVolume.js index f63078bb6d..aedcddffd0 100644 --- a/app/scripts/controllers/addConfigVolume.js +++ b/app/scripts/controllers/addConfigVolume.js @@ -20,7 +20,8 @@ angular.module('openshiftConsole') DataService, Navigate, ProjectsService, - StorageService) { + StorageService, + RELATIVE_PATH_PATTERN) { if (!$routeParams.kind || !$routeParams.name) { Navigate.toErrorPage("Kind or name parameter missing."); return; @@ -52,6 +53,7 @@ angular.module('openshiftConsole') pickKeys: false }; $scope.forms = {}; + $scope.RELATIVE_PATH_PATTERN = RELATIVE_PATH_PATTERN; $scope.breadcrumbs = BreadcrumbsService.getBreadcrumbs({ name: $routeParams.name, diff --git a/app/scripts/controllers/attachPVC.js b/app/scripts/controllers/attachPVC.js index ed2e743797..2f1e118284 100644 --- a/app/scripts/controllers/attachPVC.js +++ b/app/scripts/controllers/attachPVC.js @@ -19,7 +19,8 @@ angular.module('openshiftConsole') DataService, Navigate, ProjectsService, - StorageService) { + StorageService, + RELATIVE_PATH_PATTERN) { if (!$routeParams.kind || !$routeParams.name) { Navigate.toErrorPage("Kind or name parameter missing."); return; @@ -50,6 +51,7 @@ angular.module('openshiftConsole') $scope.projectName = $routeParams.project; $scope.kind = $routeParams.kind; $scope.name = $routeParams.name; + $scope.RELATIVE_PATH_PATTERN = RELATIVE_PATH_PATTERN; $scope.attach = { persistentVolumeClaim: null, @@ -149,11 +151,14 @@ angular.module('openshiftConsole') var persistentVolumeClaim = $scope.attach.persistentVolumeClaim; var name = $scope.attach.volumeName; var mountPath = $scope.attach.mountPath; + var subPath = $scope.attach.subPath; + var readOnly = $scope.attach.readOnly; if (mountPath) { // for each container in the pod spec, add the new volume mount angular.forEach(podTemplate.spec.containers, function(container) { if (isContainerSelected(container)) { - var newVolumeMount = StorageService.createVolumeMount(name, mountPath); + var newVolumeMount = + StorageService.createVolumeMount(name, mountPath, subPath, readOnly); if (!container.volumeMounts) { container.volumeMounts = []; } diff --git a/app/scripts/filters/resources.js b/app/scripts/filters/resources.js index ca152f2988..cea063adda 100644 --- a/app/scripts/filters/resources.js +++ b/app/scripts/filters/resources.js @@ -1422,4 +1422,28 @@ angular.module('openshiftConsole') _.has(build, 'spec.postCommit.script') || _.has(build, 'spec.postCommit.args'); }; + }) + .filter('volumeMountMode', function() { + var isConfigVolume = function(volume) { + return _.has(volume, 'configMap') || _.has(volume, 'secret'); + }; + + return function(mount, volumes) { + if (!mount) { + return ''; + } + + // Config maps and secrets are always read-only, even if not explicitly + // set in the volume mount. + var volume = _.find(volumes, { name: mount.name }); + if (isConfigVolume(volume)) { + return 'read-only'; + } + + if (_.get(volume, 'persistentVolumeClaim.readOnly')) { + return 'read-only'; + } + + return mount.readOnly ? 'read-only' : 'read-write'; + }; }); diff --git a/app/scripts/services/storage.js b/app/scripts/services/storage.js index a160b2401c..53ee4ff69a 100644 --- a/app/scripts/services/storage.js +++ b/app/scripts/services/storage.js @@ -12,11 +12,18 @@ angular.module("openshiftConsole") }; }, - createVolumeMount: function(name, mountPath) { - return { + createVolumeMount: function(name, mountPath, subPath, readOnly) { + var mount = { name: name, - mountPath: mountPath + mountPath: mountPath, + readOnly: !!readOnly }; + + if (subPath) { + mount.subPath = subPath; + } + + return mount; }, // Gets the volume names currently defined in the pod template. diff --git a/app/views/_pod-template.html b/app/views/_pod-template.html index 6faf62ce39..1cac4ea772 100644 --- a/app/views/_pod-template.html +++ b/app/views/_pod-template.html @@ -127,7 +127,8 @@
Mount: - {{mount.name}} → {{mount.mountPath}} + {{mount.name}}, subpath {{mount.subPath}} → {{mount.mountPath}} + {{mount | volumeMountMode : podTemplate.spec.volumes}}
diff --git a/app/views/add-config-volume.html b/app/views/add-config-volume.html index 14c6db5733..22c7261eb5 100644 --- a/app/views/add-config-volume.html +++ b/app/views/add-config-volume.html @@ -145,14 +145,6 @@

Keys and Paths

- Keys and Paths type="text" name="path-{{$id}}" ng-model="item.path" - ng-pattern="/^(?!\/)(?!\.\.(\/|$))(?!.*\/\.\.(\/|$)).*$/" + ng-pattern="RELATIVE_PATH_PATTERN" required osc-unique="itemPaths" placeholder="example: config/app.properties" diff --git a/app/views/attach-pvc.html b/app/views/attach-pvc.html index 6cbd395600..a2f59399e3 100644 --- a/app/views/attach-pvc.html +++ b/app/views/attach-pvc.html @@ -112,6 +112,32 @@

Volume

+
+ + +
+ Optional path within the volume from which it will be mounted into the + container. Defaults to the volume's root. +
+
+ + Path must be a relative path. It cannot start with / or + contain .. path elements. + +
+
+
diff --git a/dist/scripts/scripts.js b/dist/scripts/scripts.js index 5aa70f240e..a69b4ce2f8 100644 --- a/dist/scripts/scripts.js +++ b/dist/scripts/scripts.js @@ -555,7 +555,7 @@ screenSmMin:768, screenMdMin:992, screenLgMin:1200, screenXlgMin:1600 -}).constant("SOURCE_URL_PATTERN", /^[a-z][a-z0-9+.-@]*:(\/\/)?[0-9a-z_-]+/i).constant("IS_IOS", /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream).constant("amTimeAgoConfig", { +}).constant("SOURCE_URL_PATTERN", /^[a-z][a-z0-9+.-@]*:(\/\/)?[0-9a-z_-]+/i).constant("RELATIVE_PATH_PATTERN", /^(?!\/)(?!\.\.(\/|$))(?!.*\/\.\.(\/|$)).*$/).constant("IS_IOS", /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream).constant("amTimeAgoConfig", { titleFormat:"LLL" }).config([ "$httpProvider", "AuthServiceProvider", "RedirectLoginServiceProvider", "AUTH_CFG", "API_CFG", "kubernetesContainerSocketProvider", function(a, b, c, d, e, f) { a.interceptors.push("AuthInterceptor"), b.LoginService("RedirectLoginService"), b.LogoutService("DeleteTokenLogoutService"), b.UserStore("LocalStorageUserStore"), c.OAuthClientID(d.oauth_client_id), c.OAuthAuthorizeURI(d.oauth_authorize_uri), c.OAuthRedirectURI(URI(d.oauth_redirect_base).segment("oauth").toString()), f.WebSocketFactory = "ContainerWebSocket"; @@ -3217,11 +3217,13 @@ claimName:b.metadata.name } }; }, -createVolumeMount:function(a, b) { -return { +createVolumeMount:function(a, b, c, d) { +var e = { name:a, -mountPath:b +mountPath:b, +readOnly:!!d }; +return c && (e.subPath = c), e; }, getVolumeNames:function(a) { var b = _.get(a, "spec.volumes", []); @@ -8719,17 +8721,17 @@ details:a("getErrorDetails")(b) } }; })); -} ]), angular.module("openshiftConsole").controller("AttachPVCController", [ "$filter", "$routeParams", "$scope", "$window", "APIService", "AuthorizationService", "BreadcrumbsService", "DataService", "Navigate", "ProjectsService", "StorageService", function(a, b, c, d, e, f, g, h, i, j, k) { +} ]), angular.module("openshiftConsole").controller("AttachPVCController", [ "$filter", "$routeParams", "$scope", "$window", "APIService", "AuthorizationService", "BreadcrumbsService", "DataService", "Navigate", "ProjectsService", "StorageService", "RELATIVE_PATH_PATTERN", function(a, b, c, d, e, f, g, h, i, j, k, l) { if (!b.kind || !b.name) return void i.toErrorPage("Kind or name parameter missing."); -var l = [ "Deployment", "DeploymentConfig", "ReplicaSet", "ReplicationController" ]; -if (!_.includes(l, b.kind)) return void i.toErrorPage("Storage is not supported for kind " + b.kind + "."); -var m = { +var m = [ "Deployment", "DeploymentConfig", "ReplicaSet", "ReplicationController" ]; +if (!_.includes(m, b.kind)) return void i.toErrorPage("Storage is not supported for kind " + b.kind + "."); +var n = { resource:e.kindToResource(b.kind), group:b.group }; c.alerts = {}, c.renderOptions = { hideFilterWidget:!0 -}, c.projectName = b.project, c.kind = b.kind, c.name = b.name, c.attach = { +}, c.projectName = b.project, c.kind = b.kind, c.name = b.name, c.RELATIVE_PATH_PATTERN = l, c.attach = { persistentVolumeClaim:null, volumeName:null, mountPath:null, @@ -8742,8 +8744,8 @@ namespace:b.project, subpage:"Add Storage", includeProject:!0 }), j.get(b.project).then(_.spread(function(e, j) { -if (c.project = e, c.breadcrumbs[0].title = a("displayName")(e), !f.canI(m, "update", b.project)) return void i.toErrorPage("You do not have authority to update " + a("humanizeKind")(b.kind) + " " + b.name + ".", "access_denied"); -var l = a("orderByDisplayName"), n = a("getErrorDetails"), o = a("generateName"), p = function(a, b) { +if (c.project = e, c.breadcrumbs[0].title = a("displayName")(e), !f.canI(n, "update", b.project)) return void i.toErrorPage("You do not have authority to update " + a("humanizeKind")(b.kind) + " " + b.name + ".", "access_denied"); +var l = a("orderByDisplayName"), m = a("getErrorDetails"), o = a("generateName"), p = function(a, b) { c.disableInputs = !0, c.alerts["attach-persistent-volume-claim"] = { type:"error", message:a, @@ -8757,7 +8759,7 @@ c.existingMountPaths = k.getMountPaths(a, q); }; c.$watchGroup([ "attach.resource", "attach.allContainers" ], r), c.$watch("attach.containers", r, !0); var s = function() { -h.get(m, b.name, j).then(function(a) { +h.get(n, b.name, j).then(function(a) { c.attach.resource = a, c.breadcrumbs = g.getBreadcrumbs({ object:a, project:e, @@ -8767,7 +8769,7 @@ includeProject:!0 var b = _.get(a, "spec.template"); c.existingVolumeNames = k.getVolumeNames(b); }, function(a) { -p(b.name + " could not be loaded.", n(a)); +p(b.name + " could not be loaded.", m(a)); }), h.list("persistentvolumeclaims", j, function(a) { c.pvcs = l(a.by("metadata.name")), _.isEmpty(c.pvcs) || c.attach.persistentVolumeClaim || (c.attach.persistentVolumeClaim = _.head(c.pvcs)); }); @@ -8775,65 +8777,65 @@ c.pvcs = l(a.by("metadata.name")), _.isEmpty(c.pvcs) || c.attach.persistentVolum s(), c.attachPVC = function() { if (c.disableInputs = !0, c.attachPVCForm.$valid) { c.attach.volumeName || (c.attach.volumeName = o("volume-")); -var e = c.attach.resource, f = _.get(e, "spec.template"), g = c.attach.persistentVolumeClaim, i = c.attach.volumeName, l = c.attach.mountPath; +var e = c.attach.resource, f = _.get(e, "spec.template"), g = c.attach.persistentVolumeClaim, i = c.attach.volumeName, l = c.attach.mountPath, r = c.attach.subPath, s = c.attach.readOnly; l && angular.forEach(f.spec.containers, function(a) { if (q(a)) { -var b = k.createVolumeMount(i, l); +var b = k.createVolumeMount(i, l, r, s); a.volumeMounts || (a.volumeMounts = []), a.volumeMounts.push(b); } }); -var r = k.createVolume(i, g); -f.spec.volumes || (f.spec.volumes = []), f.spec.volumes.push(r), c.alerts = {}, h.update(m, e.metadata.name, c.attach.resource, j).then(function() { +var t = k.createVolume(i, g); +f.spec.volumes || (f.spec.volumes = []), f.spec.volumes.push(t), c.alerts = {}, h.update(n, e.metadata.name, c.attach.resource, j).then(function() { d.history.back(); }, function(d) { -p("An error occurred attaching the persistent volume claim to the " + a("humanizeKind")(b.kind) + ".", n(d)), c.disableInputs = !1; +p("An error occurred attaching the persistent volume claim to the " + a("humanizeKind")(b.kind) + ".", m(d)), c.disableInputs = !1; }); } }; })); -} ]), angular.module("openshiftConsole").controller("AddConfigVolumeController", [ "$filter", "$location", "$routeParams", "$scope", "$window", "APIService", "AuthorizationService", "BreadcrumbsService", "DataService", "Navigate", "ProjectsService", "StorageService", function(a, b, c, d, e, f, g, h, i, j, k, l) { +} ]), angular.module("openshiftConsole").controller("AddConfigVolumeController", [ "$filter", "$location", "$routeParams", "$scope", "$window", "APIService", "AuthorizationService", "BreadcrumbsService", "DataService", "Navigate", "ProjectsService", "StorageService", "RELATIVE_PATH_PATTERN", function(a, b, c, d, e, f, g, h, i, j, k, l, m) { if (!c.kind || !c.name) return void j.toErrorPage("Kind or name parameter missing."); -var m = [ "Deployment", "DeploymentConfig", "ReplicaSet", "ReplicationController" ]; -if (!_.includes(m, c.kind)) return void j.toErrorPage("Volumes are not supported for kind " + c.kind + "."); -var n = { +var n = [ "Deployment", "DeploymentConfig", "ReplicaSet", "ReplicationController" ]; +if (!_.includes(n, c.kind)) return void j.toErrorPage("Volumes are not supported for kind " + c.kind + "."); +var o = { resource:f.kindToResource(c.kind), group:c.group }; d.alerts = {}, d.projectName = c.project, d.kind = c.kind, d.name = c.name, d.attach = { allContainers:!0, pickKeys:!1 -}, d.forms = {}, d.breadcrumbs = h.getBreadcrumbs({ +}, d.forms = {}, d.RELATIVE_PATH_PATTERN = m, d.breadcrumbs = h.getBreadcrumbs({ name:c.name, kind:c.kind, namespace:c.project, subpage:"Add Config Files", includeProject:!0 }), d.returnURL = b.url(); -var o = a("humanizeKind"); +var p = a("humanizeKind"); d.groupByKind = function(a) { -return o(a.kind); +return p(a.kind); }; -var p = function() { +var q = function() { _.set(d, "attach.items", [ {} ]); }; -d.$watch("attach.source", p); -var q = function() { +d.$watch("attach.source", q); +var r = function() { d.forms.addConfigVolumeForm.$setDirty(); }; d.addItem = function() { -d.attach.items.push({}), q(); +d.attach.items.push({}), r(); }, d.removeItem = function(a) { -d.attach.items.splice(a, 1), q(); +d.attach.items.splice(a, 1), r(); }, k.get(c.project).then(_.spread(function(b, f) { -if (d.project = b, !g.canI(n, "update", c.project)) return void j.toErrorPage("You do not have authority to update " + o(c.kind) + " " + c.name + ".", "access_denied"); -var k = a("orderByDisplayName"), m = a("getErrorDetails"), p = a("generateName"), q = function(a, b) { +if (d.project = b, !g.canI(o, "update", c.project)) return void j.toErrorPage("You do not have authority to update " + p(c.kind) + " " + c.name + ".", "access_denied"); +var k = a("orderByDisplayName"), m = a("getErrorDetails"), n = a("generateName"), q = function(a, b) { d.alerts["attach-persistent-volume-claim"] = { type:"error", message:a, details:b }; }; -i.get(n, c.name, f, { +i.get(o, c.name, f, { errorNotification:!1 }).then(function(a) { d.targetObject = a, d.breadcrumbs = h.getBreadcrumbs({ @@ -8870,31 +8872,31 @@ d.itemPaths = _.compact(a); }; d.$watch("attach.items", t, !0), d.addVolume = function() { if (!d.forms.addConfigVolumeForm.$invalid) { -var b = d.targetObject, g = _.get(d, "attach.source"), h = _.get(b, "spec.template"), j = p("volume-"), k = _.get(d, "attach.mountPath"), l = { +var b = d.targetObject, g = _.get(d, "attach.source"), h = _.get(b, "spec.template"), j = n("volume-"), k = _.get(d, "attach.mountPath"), l = { name:j, mountPath:k }; "Secret" === g.kind && (l.readOnly = !0), _.each(h.spec.containers, function(a) { r(a) && (a.volumeMounts = a.volumeMounts || [], a.volumeMounts.push(l)); }); -var o, s = { +var p, s = { name:j }; -switch (d.attach.pickKeys && (o = d.attach.items), g.kind) { +switch (d.attach.pickKeys && (p = d.attach.items), g.kind) { case "ConfigMap": s.configMap = { name:g.metadata.name, -items:o +items:p }; break; case "Secret": s.secret = { secretName:g.metadata.name, -items:o +items:p }; } -h.spec.volumes = h.spec.volumes || [], h.spec.volumes.push(s), d.alerts = {}, d.disableInputs = !0, i.update(n, b.metadata.name, d.targetObject, f).then(function() { +h.spec.volumes = h.spec.volumes || [], h.spec.volumes.push(s), d.alerts = {}, d.disableInputs = !0, i.update(o, b.metadata.name, d.targetObject, f).then(function() { e.history.back(); }, function(b) { d.disableInputs = !1; @@ -14357,6 +14359,17 @@ return c ? "#" + c :"Unknown"; return function(a) { return _.has(a, "spec.postCommit.command") || _.has(a, "spec.postCommit.script") || _.has(a, "spec.postCommit.args"); }; +}).filter("volumeMountMode", function() { +var a = function(a) { +return _.has(a, "configMap") || _.has(a, "secret"); +}; +return function(b, c) { +if (!b) return ""; +var d = _.find(c, { +name:b.name +}); +return a(d) ? "read-only" :_.get(d, "persistentVolumeClaim.readOnly") ? "read-only" :b.readOnly ? "read-only" :"read-write"; +}; }), angular.module("openshiftConsole").filter("canI", [ "AuthorizationService", function(a) { return function(b, c, d) { return a.canI(b, c, d); diff --git a/dist/scripts/templates.js b/dist/scripts/templates.js index a4419ca4a5..647960dc03 100644 --- a/dist/scripts/templates.js +++ b/dist/scripts/templates.js @@ -405,7 +405,8 @@ angular.module('openshiftConsoleTemplates', []).run(['$templateCache', function( "
\n" + "Mount:\n" + "\n" + - "{{mount.name}} → {{mount.mountPath}}\n" + + "{{mount.name}}, subpath {{mount.subPath}} → {{mount.mountPath}}\n" + + "{{mount | volumeMountMode : podTemplate.spec.volumes}}\n" + "\n" + "
\n" + "
\n" + @@ -1122,8 +1123,7 @@ angular.module('openshiftConsoleTemplates', []).run(['$templateCache', function( "
\n" + "
\n" + "\n" + - "\n" + - "\n" + + "\n" + "
\n" + "\n" + "Path must be a relative path. It cannot start with / or contain .. path elements.\n" + @@ -1264,6 +1264,18 @@ angular.module('openshiftConsoleTemplates', []).run(['$templateCache', function( "
\n" + "
\n" + "
\n" + + "\n" + + "\n" + + "
\n" + + "Optional path within the volume from which it will be mounted into the container. Defaults to the volume's root.\n" + + "
\n" + + "
\n" + + "\n" + + "Path must be a relative path. It cannot start with / or contain .. path elements.\n" + + "\n" + + "
\n" + + "
\n" + + "
\n" + "\n" + "\n" + "\n" + @@ -1286,6 +1298,17 @@ angular.module('openshiftConsoleTemplates', []).run(['$templateCache', function( "\n" + "
\n" + "
\n" + + "
\n" + + "
\n" + + "\n" + + "
\n" + + "Mount the volume as read-only.\n" + + "
\n" + + "
\n" + + "
\n" + "\n" + "
1\">\n" + "
\n" +