mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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:
3
.changelog/26438.txt
Normal file
3
.changelog/26438.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
sentinel (Enterprise): Added policy scope for csi-volumes
|
||||
```
|
||||
48
api/csi.go
48
api/csi.go
@@ -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
|
||||
|
||||
@@ -87,4 +87,5 @@ type SentinelPolicyListStub struct {
|
||||
const (
|
||||
SentinelScopeSubmitJob = "submit-job"
|
||||
SentinelScopeSubmitHostVolume = "submit-host-volume"
|
||||
SentinelScopeSubmitCSIVolume = "submit-csi-volume"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
62
command/volume_create_csi_test.go
Normal file
62
command/volume_create_csi_test.go
Normal 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")
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
16
nomad/csi_endpoint_ce.go
Normal 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
|
||||
}
|
||||
@@ -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])",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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]"})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user