Add CSI Volume Sentinel Policy scaffolding (#26438)

* Add ent policy enforcement stubs to CSI Volume create/register

* Wire policy override/warnings through CSI volume register/create

* Add new scope to sentinel apply

* Sanitize CSISecrets & CSIMountOptions

* Add sentinel policy scope to ui

* Update docs for new sentinel scope/policy

* Create new api funcs for CSI endpoints

* fix sentinel csi ui test

* Update sentinel-policy docs

* Add changelog

* Update docs from feedback
This commit is contained in:
Allison Larson
2025-08-07 12:03:18 -07:00
committed by GitHub
parent 79bf619833
commit e16a3339ad
24 changed files with 396 additions and 48 deletions

3
.changelog/26438.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
sentinel (Enterprise): Added policy scope for csi-volumes
```

View File

@@ -76,13 +76,27 @@ func (v *CSIVolumes) Info(id string, q *QueryOptions) (*CSIVolume, *QueryMeta, e
// Register registers a single CSIVolume with Nomad. The volume must already
// exist in the external storage provider.
func (v *CSIVolumes) Register(vol *CSIVolume, w *WriteOptions) (*WriteMeta, error) {
req := CSIVolumeRegisterRequest{
req := &CSIVolumeRegisterRequest{
Volumes: []*CSIVolume{vol},
}
meta, err := v.client.put("/v1/volume/csi/"+vol.ID, req, nil, w)
_, meta, err := v.RegisterOpts(req, w)
return meta, err
}
// RegisterOpts registers a single CSIVolume with Nomad. The volume must already
// exist in the external storage provider. It expects a single volume in the
// request.
func (v *CSIVolumes) RegisterOpts(req *CSIVolumeRegisterRequest, w *WriteOptions) (*CSIVolumeRegisterResponse, *WriteMeta, error) {
if w == nil {
w = &WriteOptions{}
}
vol := req.Volumes[0]
resp := &CSIVolumeRegisterResponse{}
meta, err := v.client.put("/v1/volume/csi/"+vol.ID, req, resp, w)
return resp, meta, err
}
// Deregister deregisters a single CSIVolume from Nomad. The volume will not be deleted from the external storage provider.
func (v *CSIVolumes) Deregister(id string, force bool, w *WriteOptions) error {
_, err := v.client.delete(fmt.Sprintf("/v1/volume/csi/%v?force=%t", url.PathEscape(id), force), nil, nil, w)
@@ -97,9 +111,21 @@ func (v *CSIVolumes) Create(vol *CSIVolume, w *WriteOptions) ([]*CSIVolume, *Wri
Volumes: []*CSIVolume{vol},
}
resp, meta, err := v.CreateOpts(&req, w)
return resp.Volumes, meta, err
}
// CreateOpts creates a single CSIVolume in an external storage provider and
// registers it with Nomad. You do not need to call Register if this call is
// successful. It expects a single volume in the request.
func (v *CSIVolumes) CreateOpts(req *CSIVolumeCreateRequest, w *WriteOptions) (*CSIVolumeCreateResponse, *WriteMeta, error) {
if w == nil {
w = &WriteOptions{}
}
vol := req.Volumes[0]
resp := &CSIVolumeCreateResponse{}
meta, err := v.client.put(fmt.Sprintf("/v1/volume/csi/%v/create", vol.ID), req, resp, w)
return resp.Volumes, meta, err
return resp, meta, err
}
// Delete deletes a CSI volume from an external storage provider. The ID
@@ -452,19 +478,33 @@ func (v CSIVolumeExternalStubSort) Swap(i, j int) {
type CSIVolumeCreateRequest struct {
Volumes []*CSIVolume
// PolicyOverride overrides Sentinel soft-mandatory policy enforcement
PolicyOverride bool
WriteRequest
}
type CSIVolumeCreateResponse struct {
Volumes []*CSIVolume
Volumes []*CSIVolume
Warnings string
QueryMeta
}
type CSIVolumeRegisterRequest struct {
Volumes []*CSIVolume
// PolicyOverride overrides Sentinel soft-mandatory policy enforcement
PolicyOverride bool
WriteRequest
}
type CSIVolumeRegisterResponse struct {
Volumes []*CSIVolume
Warnings string
}
type CSIVolumeDeregisterRequest struct {
VolumeIDs []string
WriteRequest

View File

@@ -87,4 +87,5 @@ type SentinelPolicyListStub struct {
const (
SentinelScopeSubmitJob = "submit-job"
SentinelScopeSubmitHostVolume = "submit-host-volume"
SentinelScopeSubmitCSIVolume = "submit-csi-volume"
)

View File

@@ -39,7 +39,7 @@ Apply Options:
-scope
Sets the scope of the policy and when it should be enforced. One of
"submit-job" or "submit-host-volume".
"submit-job", "submit-host-volume" or "submit-csi-volume".
-level (default: advisory)
Sets the enforcement level of the policy. Must be one of advisory,
@@ -109,7 +109,7 @@ func (c *SentinelApplyCommand) Run(args []string) int {
}
switch scope {
case api.SentinelScopeSubmitJob, api.SentinelScopeSubmitHostVolume:
case api.SentinelScopeSubmitJob, api.SentinelScopeSubmitHostVolume, api.SentinelScopeSubmitCSIVolume:
case "":
c.Ui.Error("-scope flag is required")
return 1

View File

@@ -51,8 +51,7 @@ Create Options:
volumes only.
-policy-override
Sets the flag to force override any soft mandatory Sentinel policies. Used
for dynamic host volumes only.
Sets the flag to force override any soft mandatory Sentinel policies.
`
return strings.TrimSpace(helpText)
@@ -134,7 +133,7 @@ func (c *VolumeCreateCommand) Run(args []string) int {
switch strings.ToLower(volType) {
case "csi":
return c.csiCreate(client, ast)
return c.csiCreate(client, ast, override)
case "host":
return c.hostVolumeCreate(client, ast, detach, verbose, override, volID)
default:

View File

@@ -10,19 +10,29 @@ import (
"github.com/hashicorp/nomad/api"
)
func (c *VolumeCreateCommand) csiCreate(client *api.Client, ast *ast.File) int {
func (c *VolumeCreateCommand) csiCreate(client *api.Client, ast *ast.File, override bool) int {
vol, err := csiDecodeVolume(ast)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error decoding the volume definition: %s", err))
return 1
}
vols, _, err := client.CSIVolumes().Create(vol, nil)
resp, _, err := client.CSIVolumes().CreateOpts(&api.CSIVolumeCreateRequest{
Volumes: []*api.CSIVolume{vol},
PolicyOverride: override,
}, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error creating volume: %s", err))
return 1
}
for _, vol := range vols {
if resp.Warnings != "" {
c.Ui.Output(
c.Colorize().Color(
fmt.Sprintf("[bold][yellow]Volume Warnings:\n%s[reset]\n", resp.Warnings)))
}
for _, vol := range resp.Volumes {
// note: the command only ever returns 1 volume from the API
c.Ui.Output(fmt.Sprintf(
"Created external volume %s with ID %s", vol.ExternalID, vol.ID))

View File

@@ -0,0 +1,62 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"os"
"testing"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
)
func TestVolumeCreateCommand_Run(t *testing.T) {
ci.Parallel(t)
srv, client, url := testServer(t, true, nil)
t.Cleanup(srv.Shutdown)
waitForNodes(t, client)
_, err := client.Namespaces().Register(&api.Namespace{Name: "prod"}, nil)
must.NoError(t, err)
ui := cli.NewMockUi()
cmd := &VolumeCreateCommand{
Meta: Meta{Ui: ui},
}
volumeHCL := `
type = "csi"
id = "test-volume"
name = "test-volume"
external_id = "external-test-volume"
plugin_id = "test-plugin"
capacity_min = "1GiB"
capacity_max = "10GiB"
capability {
access_mode = "single-node-reader-only"
attachment_mode = "block-device"
}
`
file, err := os.CreateTemp(t.TempDir(), "csi-volume-test-*.hcl")
must.NoError(t, err)
_, err = file.WriteString(volumeHCL)
must.NoError(t, err)
// Since we can't easily mock the API client to fake a CSI plugin running,
// we'll expect this to fail with a plugin-related error. The flow and
// parsing can still be tested.
args := []string{"-address", url, file.Name()}
code := cmd.Run(args)
must.Eq(t, 1, code)
// Verify error output contains expected message about volume creation
output := ui.ErrorWriter.String()
must.StrContains(t, output, "Error creating volume")
must.StrContains(t, output, "no CSI plugin named: test-plugin could be found")
}

View File

@@ -43,8 +43,7 @@ Register Options:
host volumes only.
-policy-override
Sets the flag to force override any soft mandatory Sentinel policies. Used
for dynamic host volumes only.
Sets the flag to force override any soft mandatory Sentinel policies.
`
return strings.TrimSpace(helpText)
@@ -123,7 +122,7 @@ func (c *VolumeRegisterCommand) Run(args []string) int {
switch volType {
case "csi":
return c.csiRegister(client, ast)
return c.csiRegister(client, ast, override)
case "host":
return c.hostVolumeRegister(client, ast, override, volID)
default:

View File

@@ -15,17 +15,27 @@ import (
"github.com/mitchellh/mapstructure"
)
func (c *VolumeRegisterCommand) csiRegister(client *api.Client, ast *ast.File) int {
func (c *VolumeRegisterCommand) csiRegister(client *api.Client, ast *ast.File, override bool) int {
vol, err := csiDecodeVolume(ast)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error decoding the volume definition: %s", err))
return 1
}
_, err = client.CSIVolumes().Register(vol, nil)
resp, _, err := client.CSIVolumes().RegisterOpts(&api.CSIVolumeRegisterRequest{
Volumes: []*api.CSIVolume{vol},
PolicyOverride: override,
}, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error registering volume: %s", err))
return 1
}
if resp.Warnings != "" {
c.Ui.Output(
c.Colorize().Color(
fmt.Sprintf("[bold][yellow]Volume Warnings:\n%s[reset]\n", resp.Warnings)))
}
vol = resp.Volumes[0] // note: the command only ever returns 1 volume from the API
c.Ui.Output(fmt.Sprintf("Volume %q registered", vol.ID))
return 0

View File

@@ -4,8 +4,10 @@
package command
import (
"os"
"testing"
"github.com/hashicorp/cli"
"github.com/hashicorp/hcl"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
@@ -203,3 +205,52 @@ topology_request {
})
}
}
func TestVolumeRegisterCommand_Run(t *testing.T) {
ci.Parallel(t)
srv, client, url := testServer(t, true, nil)
t.Cleanup(srv.Shutdown)
waitForNodes(t, client)
_, err := client.Namespaces().Register(&api.Namespace{Name: "prod"}, nil)
must.NoError(t, err)
ui := cli.NewMockUi()
cmd := &VolumeRegisterCommand{
Meta: Meta{Ui: ui},
}
volumeHCL := `
type = "csi"
id = "test-volume"
name = "test-volume"
external_id = "external-test-volume"
plugin_id = "test-plugin"
capacity_min = "1GiB"
capacity_max = "10GiB"
capability {
access_mode = "single-node-reader-only"
attachment_mode = "block-device"
}
`
file, err := os.CreateTemp(t.TempDir(), "csi-volume-test-*.hcl")
must.NoError(t, err)
_, err = file.WriteString(volumeHCL)
must.NoError(t, err)
// Since we can't easily mock the API client to fake a CSI plugin running,
// we'll expect this to fail with a plugin-related error. The flow and
// parsing can still be tested.
args := []string{"-address", url, file.Name()}
code := cmd.Run(args)
must.Eq(t, 1, code)
// Verify error output contains expected message about volume creation
output := ui.ErrorWriter.String()
must.StrContains(t, output, "Error registering volume")
must.StrContains(t, output, "no CSI plugin named: test-plugin could be found")
}

View File

@@ -354,6 +354,15 @@ func (v *CSIVolume) Register(args *structs.CSIVolumeRegisterRequest, reply *stru
if err := v.controllerValidateVolume(args, vol, plugin); err != nil {
return err
}
warn, err := v.enforceEnterprisePolicy(snap, vol, existingVol, args.GetIdentity().GetACLToken(), args.PolicyOverride)
if warn != nil {
reply.Warnings = warn.Error()
}
if err != nil {
return err
}
}
_, index, err := v.srv.raftApply(structs.CSIVolumeRegisterRequestType, args)
@@ -1093,6 +1102,15 @@ func (v *CSIVolume) Create(args *structs.CSIVolumeCreateRequest, reply *structs.
validatedVols = append(validatedVols,
validated{vol, plugin, current})
warn, err := v.enforceEnterprisePolicy(snap, vol, current, args.GetIdentity().GetACLToken(), args.PolicyOverride)
if warn != nil {
reply.Warnings = warn.Error()
}
if err != nil {
return err
}
}
// Attempt to create all the validated volumes and write only successfully

16
nomad/csi_endpoint_ce.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !ent
// +build !ent
package nomad
import (
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
)
func (v *CSIVolume) enforceEnterprisePolicy(_ *state.StateSnapshot, _ *structs.CSIVolume, _ *structs.CSIVolume, _ *structs.ACLToken, _ bool) (error, error) {
return nil, nil
}

View File

@@ -285,6 +285,7 @@ func TestCSIVolumeEndpoint_Register(t *testing.T) {
err = msgpackrpc.CallWithCodec(codec, "CSIVolume.Register", req1, resp1)
must.NoError(t, err)
must.NotEq(t, uint64(0), resp1.Index)
must.Eq(t, "", resp1.Warnings)
// Get the volume back out
req2 := &structs.CSIVolumeGetRequest{
@@ -1282,25 +1283,11 @@ func TestCSIVolumeEndpoint_Create(t *testing.T) {
must.NoError(t, err)
must.NotEq(t, uint64(0), resp1.Index)
// Get the volume back out
req2 := &structs.CSIVolumeGetRequest{
ID: volID,
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: ns,
AuthToken: validToken,
},
}
resp2 := &structs.CSIVolumeGetResponse{}
err = msgpackrpc.CallWithCodec(codec, "CSIVolume.Get", req2, resp2)
must.NoError(t, err)
must.Eq(t, resp1.Index, resp2.Index)
// Check the new volume in the response
must.Eq(t, 1, len(resp1.Volumes))
must.Eq(t, "", resp1.Warnings)
vol := resp1.Volumes[0]
vol := resp2.Volume
must.NotNil(t, vol)
must.Eq(t, volID, vol.ID)
// these fields are set from the args
must.Eq(t, "csi.CSISecrets(map[mysecret:[REDACTED]])",
vol.Secrets.String())
must.Eq(t, "csi.CSIOptions(FSType: ext4, MountFlags: [REDACTED])",

View File

@@ -204,6 +204,15 @@ func (o *CSIMountOptions) GoString() string {
return o.String()
}
// Sanitize returns a copy of the CSIMountOptions with sensitive data redacted
func (o *CSIMountOptions) Sanitize() *CSIMountOptions {
redacted := *o
if len(o.MountFlags) != 0 {
redacted.MountFlags = []string{"[REDACTED]"}
}
return &redacted
}
// CSISecrets contain optional additional configuration that can be used
// when specifying that a Volume should be used with VolumeAccessTypeMount.
type CSISecrets map[string]string
@@ -225,6 +234,15 @@ func (s *CSISecrets) GoString() string {
return s.String()
}
// Sanitize returns a copy of the CSISecrets with sensitive data redacted
func (s *CSISecrets) Sanitize() *CSISecrets {
redacted := CSISecrets{}
for k := range *s {
redacted[k] = "[REDACTED]"
}
return &redacted
}
type CSIVolumeClaim struct {
AllocationID string
NodeID string
@@ -852,10 +870,18 @@ func (v *CSIVolume) Merge(other *CSIVolume) error {
type CSIVolumeRegisterRequest struct {
Volumes []*CSIVolume
Timestamp int64 // UnixNano
// PolicyOverride is set when the user is attempting to override any
// Enterprise policy enforcement
PolicyOverride bool
WriteRequest
}
type CSIVolumeRegisterResponse struct {
// Warnings are non-fatal messages from Enterprise policy enforcement
Warnings string
QueryMeta
}
@@ -872,11 +898,19 @@ type CSIVolumeDeregisterResponse struct {
type CSIVolumeCreateRequest struct {
Volumes []*CSIVolume
Timestamp int64 // UnixNano
// PolicyOverride is set when the user is attempting to override any
// Enterprise policy enforcement
PolicyOverride bool
WriteRequest
}
type CSIVolumeCreateResponse struct {
Volumes []*CSIVolume
// Warnings are non-fatal messages from Enterprise policy enforcement
Warnings string
QueryMeta
}

View File

@@ -1092,3 +1092,34 @@ func TestTaskCSIPluginConfig_Equal(t *testing.T) {
Apply: func(c *TaskCSIPluginConfig) { c.HealthTimeout = 1 * time.Second },
}})
}
func TestCSISecretsSanitize(t *testing.T) {
ci.Parallel(t)
orig := &CSISecrets{
"foo": "bar",
"baz": "qux",
}
sanitized := orig.Sanitize()
must.NotEq(t, orig, sanitized)
for _, v := range *sanitized {
must.Eq(t, v, "[REDACTED]")
}
}
func TestCSIMountOptionsSanitize(t *testing.T) {
ci.Parallel(t)
orig := &CSIMountOptions{
FSType: "ext4",
MountFlags: []string{"ro", "noatime"},
}
sanitized := orig.Sanitize()
must.NotEq(t, orig, sanitized)
must.Eq(t, sanitized.FSType, orig.FSType)
must.Eq(t, sanitized.MountFlags, []string{"[REDACTED]"})
}

View File

@@ -85,6 +85,9 @@
</G.RadioField>
<G.RadioField @id="submit-host-volume" checked={{eq @policy.scope "submit-host-volume"}} data-test-scope="submit-host-volume" as |F|>
<F.Label>Submit Host Volume</F.Label>
</G.RadioField>
<G.RadioField @id="submit-csi-volume" checked={{eq @policy.scope "submit-csi-volume"}} data-test-scope="submit-csi-volume" as |F|>
<F.Label>Submit CSI Volume</F.Label>
</G.RadioField>
</Hds::Form::Radio::Group>
</div>

View File

@@ -22,6 +22,6 @@ export default Factory.extend({
main = rule { false }`,
scope: pickOne(['submit-job', 'submit-host-volume']),
scope: pickOne(['submit-job', 'submit-host-volume', 'submit-csi-volume']),
enforcementLevel: pickOne(['advisory', 'soft-mandatory', 'hard-mandatory']),
});

View File

@@ -718,6 +718,23 @@ main = rule { has_tag() }
scope: 'submit-host-volume',
});
server.createList('sentinel-policy', 5);
server.create('sentinel-policy', {
id: 'csi-volume-policy',
name: 'csi-volume-policy',
description: 'A sentinel policy generated by Mirage',
enforcementLevel: 'soft-mandatory',
policy: `
has_tag = func() {
print("volume is missing tag")
tag = volume.parameters["tag"] else 0
return tag is not 0
}
main = rule { has_tag() }
`,
scope: 'submit-csi-volume',
});
server.createList('sentinel-policy', 5);
}
faker.seed(1);

View File

@@ -128,6 +128,28 @@ module('Acceptance | sentinel policies', function (hooks) {
assert
.dom(policyRow.querySelector('[data-test-sentinel-policy-scope]'))
.hasText('submit-host-volume');
const policyCsi = server.db.sentinelPolicies.findBy(
(sp) => sp.name === 'csi-volume-policy'
);
await click('[data-test-sentinel-policy-name="csi-volume-policy"]');
assert.equal(
currentURL(),
`/administration/sentinel-policies/${policyCsi.id}`
);
await click('[data-test-scope="submit-csi-volume"]');
await click('button[data-test-save-policy]');
assert.dom('.flash-message.alert-success').exists();
await Administration.visitSentinelPolicies();
const policyRowCsi = find(
'[data-test-sentinel-policy-name="csi-volume-policy"]'
).closest('[data-test-sentinel-policy-row]');
assert.dom(policyRowCsi).exists();
assert
.dom(policyRowCsi.querySelector('[data-test-sentinel-policy-scope]'))
.hasText('submit-csi-volume');
});
test('New Sentinel Policy from Scratch', async function (assert) {

View File

@@ -313,7 +313,7 @@ The following table shows this endpoint's support for [blocking queries][] and
| Blocking Queries | ACL Required |
| ---------------- | ---------------------------- |
| `NO` | `namespace:csi-write-volume` |
| `NO` | `namespace:csi-write-volume`. <br/>`namespace:sentinel-override` if `PolicyOverride` set |
### Parameters
@@ -321,13 +321,20 @@ The following table shows this endpoint's support for [blocking queries][] and
volume. This must be the full ID. Specify this as part of the
path.
### Sample Payload
- `Volumes` <code>([Volumes][csi-volume-spec]:</code> `<required>`) - Specifies
the JSON definition of the CSI volume. You should include the ID field if you
are updating an existing volume. Note that you must include the `NodeID` field
for the register API.
The payload must include a JSON document that describes the volume's
parameters. Note that the `NodeID` field is required for the register API.
- `PolicyOverride` `(bool: false)` - If set, Nomad overrides any soft mandatory
Sentinel policies. This field allows creating a volume when it would be denied
by policy.
### Sample Payload
```json
{
"PolicyOverride": false,
"Volumes": [
{
"ExternalID": "vol-abcdef",
@@ -382,7 +389,7 @@ The following table shows this endpoint's support for [blocking queries][] and
| Blocking Queries | ACL Required |
| ---------------- | ---------------------------- |
| `NO` | `namespace:csi-write-volume` |
| `NO` | `namespace:csi-write-volume`. <br/>`namespace:sentinel-override` if `PolicyOverride` set |
### Parameters
@@ -390,13 +397,19 @@ The following table shows this endpoint's support for [blocking queries][] and
volume. This must be the full ID. Specify this as part of the
path.
### Sample Payload
- `Volumes` <code>([Volumes][csi-volume-spec]:</code> `<required>`) - Specifies
the JSON definition of the CSI volume. You should include the ID field if you
are updating an existing volume.
The payload must include a JSON document that describes the volume's
parameters.
- `PolicyOverride` `(bool: false)` - If set, Nomad overrides any soft mandatory
Sentinel policies. This field allows creating a volume when it would be denied
by policy.
### Sample Payload
```json
{
"PolicyOverride": false,
"Volumes": [
{
"ID": "volume-id1",
@@ -1226,3 +1239,4 @@ $ curl \
[csi_plugins_internals]: /nomad/docs/architecture/storage/csi
[Create CSI Volume]: #create-csi-volume
[Volume Expansion]: /nomad/docs/other-specifications/volume/csi#volume-expansion
[csi-volume-spec]: /nomad/docs/other-specifications/volume/csi

View File

@@ -38,6 +38,7 @@ requires a management token.
- The `submit-job` scope for registering jobs
- The `submit-host-volume` scope for creating or updating dynamic host
volumes.
- The `submit-csi-volume` scope for creating or updating CSI volumes.
Refer to the [Sentinel guide](/nomad/docs/reference/sentinel-policy) for scope details.

View File

@@ -45,8 +45,7 @@ volumes or `host-volume-create` for dynamic host volumes.
dynamic host volumes only. Not valid for CSI volumes.
- `-policy-override`: Sets the flag to force override any soft mandatory
Sentinel policies. Used for dynamic host volumes only. Not valid for CSI
volumes.
Sentinel policies.
## Volume specification

View File

@@ -35,8 +35,7 @@ volumes or `host-volume-register` for dynamic host volumes.
## Options
- `-policy-override`: Sets the flag to force override any soft mandatory
Sentinel policies. Used for dynamic host volumes only. Not valid for CSI
volumes.
Sentinel policies.
## Volume specification
@@ -113,3 +112,5 @@ the exact section.
[csi_plugins_internals]: /nomad/docs/architecture/storage/csi
[volume_specification]: /nomad/docs/other-specifications/volume
[`volume create`]: /nomad/commands/volume/create
[csi_vol_spec]: /nomad/docs/other-specifications/volume/csi
[host_vol_spec]: /nomad/docs/other-specifications/volume/host

View File

@@ -74,6 +74,23 @@ The following top-level objects are available to policies in the
* `node_pool`: the node pool of the node the volume has been placed on. This is
a [Sentinel Node Pool Object](#sentinel-node-pool-objects).
### submit-csi-volume scope
The following top-level objects are available to policies in the
`submit-csi-volume` scope automatically, without an explicit import.
- `volume`: the submitted volume. This is a [Sentinel CSI Volume
Object](#sentinel-csi-volume-objects).
- `existing_volume`: the previous version of the volume. If `volume_exists` is
true, this is always non-nil. This is also a [Sentinel CSI Volume
Object](#sentinel-csi-volume-objects).
- `volume_exists`: a boolean field that indicates that a previous version of the
volume exists.
- `nomad_acl_token`: the ACL token the job was submitted with. This is a
[Sentinel Nomad ACL Token Object](#sentinel-acl-token-objects).
- `namespace`: the namespace the job is in. This is a [Sentinel Nomad Namespace
Object](#sentinel-namespace-objects).
Sentinel convention for identifiers is lower case and separated by underscores.
All fields on an object are accessed by the same name, converted to lower case
and separating camel case to underscores.
@@ -143,6 +160,18 @@ converted to the Sentinel convention. Here are some examples:
| `volume.Name` | `volume.name` |
| `volume.RequestedCapabilities[0].AccessMode` | `volume.requested_capabilities[0].access_mode` |
## Sentinel CSI Volume Objects
The `volume` object maps to the [CSI Volume][], with the fields
converted to the Sentinel convention. Here are some examples:
| CSI Volume Field | Sentinel Accessor |
|----------------------------------------------|------------------------------------------------|
| `volume.Name` | `volume.name` |
| `volume.RequestedCapabilities[0].AccessMode` | `volume.requested_capabilities[0].access_mode` |
Note that the `volume.Secrets` values and `volume.MountOptions.MountFlags` are always redacted to prevent credential leaks.
[Nomad Sentinel Tutorial]: /nomad/docs/govern/sentinel
[`nomad sentinel`]: /nomad/commands/sentinel
@@ -153,3 +182,4 @@ converted to the Sentinel convention. Here are some examples:
[Node]: https://github.com/hashicorp/nomad/blob/v1.9.4/nomad/structs/structs.go#L2086-L2210
[Node Pool]: https://github.com/hashicorp/nomad/blob/v1.9.4/nomad/structs/node_pool.go#L46-L68
[Dynamic Host Volume]: https://github.com/hashicorp/nomad/blob/main/nomad/structs/host_volumes.go#L18-L87
[CSI Volume]: https://github.com/hashicorp/nomad/blob/main/nomad/structs/csi.go#L249-L312