Support for sharing files with the host system (#103)

* Support for sharing files with the host system

* Integration tests

* Added back TestVMGarbageCollection comment
This commit is contained in:
Nikolay Edigaryev
2023-07-04 18:10:53 +04:00
committed by GitHub
parent 6a325daf74
commit d57d18d380
24 changed files with 764 additions and 2 deletions
+48
View File
@@ -16,6 +16,29 @@ paths:
application/json:
schema:
$ref: '#components/schemas/ControllerInfo'
/cluster-settings:
get:
summary: "Retrieve cluster settings"
tags:
- cluster-settings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#components/schemas/ClusterSettings'
put:
summary: "Update cluster settings"
tags:
- cluster-settings
responses:
'200':
description: Cluster settings were successfully updated
content:
application/json:
schema:
$ref: '#components/schemas/ClusterSettings'
/service-accounts:
post:
summary: "Create a Service Account"
@@ -301,6 +324,17 @@ components:
for this VM to be scheduled
additionalProperties:
type: integer
hostDirs:
type: array
items:
type: object
properties:
name:
type: string
path:
type: string
ro:
type: boolean
Events:
title: Events
type: object
@@ -343,3 +377,17 @@ components:
commit:
type: string
description: Commit hash
ClusterSettings:
title: Cluster settings
type: object
properties:
hostDirPolicies:
type: array
description: If not empty, enables instantiating VMs with hostDir volumes that match the policies listed in this array.
items:
type: object
properties:
pathPrefix:
type: string
ro:
type: boolean
+17
View File
@@ -21,6 +21,7 @@ var headless bool
var resources map[string]string
var restartPolicy string
var startupScript string
var hostDirsRaw []string
func newCreateVMCommand() *cobra.Command {
command := &cobra.Command{
@@ -44,6 +45,9 @@ func newCreateVMCommand() *cobra.Command {
command.PersistentFlags().StringVar(&startupScript, "startup-script", "",
"startup script (e.g. --startup-script=\"sync\") or a path to a script file prefixed with \"@\" "+
"(e.g. \"--startup-script=@script.sh\")")
command.PersistentFlags().StringSliceVar(&hostDirsRaw, "host-dirs", []string{},
"host directories to mount to the VM, can be specified multiple times and/or be comma-separated "+
"(see \"tart run\"'s --dir argument for syntax)")
return command
}
@@ -51,6 +55,18 @@ func newCreateVMCommand() *cobra.Command {
func runCreateVM(cmd *cobra.Command, args []string) error {
name := args[0]
// Convert arguments
var hostDirs []v1.HostDir
for _, hostDirRaw := range hostDirsRaw {
hostDir, err := v1.NewHostDirFromString(hostDirRaw)
if err != nil {
return err
}
hostDirs = append(hostDirs, hostDir)
}
vm := &v1.VM{
Meta: v1.Meta{
Name: name,
@@ -61,6 +77,7 @@ func runCreateVM(cmd *cobra.Command, args []string) error {
NetSoftnet: netSoftnet,
NetBridged: netBridged,
Headless: headless,
HostDirs: hostDirs,
}
// Convert resources
+50
View File
@@ -0,0 +1,50 @@
package get
import (
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gosuri/uitable"
"github.com/samber/lo"
"github.com/spf13/cobra"
"strings"
)
func newGetClusterSettingsCommand() *cobra.Command {
command := &cobra.Command{
Use: "cluster-settings",
Short: "Retrieve cluster settings",
RunE: runGetClusterSettings,
}
return command
}
func runGetClusterSettings(cmd *cobra.Command, args []string) error {
client, err := client.New()
if err != nil {
return err
}
clusterSettings, err := client.ClusterSettings().Get(cmd.Context())
if err != nil {
return err
}
table := uitable.New()
table.AddRow("Key", "Value")
hostDirPoliciesAsStrings := lo.Map(clusterSettings.HostDirPolicies, func(policy v1.HostDirPolicy, _ int) string {
return policy.String()
})
hostDirPoliciesDescription := strings.Join(hostDirPoliciesAsStrings, ",")
if hostDirPoliciesDescription == "" {
hostDirPoliciesDescription = "none"
}
table.AddRow("hostDir policies", hostDirPoliciesDescription)
fmt.Println(table)
return nil
}
+5 -1
View File
@@ -13,7 +13,11 @@ func NewCommand() *cobra.Command {
Short: "Retrieve resources from the controller",
}
command.AddCommand(newGetServiceAccountCommand(), newGetBootstrapTokenCommand())
command.AddCommand(
newGetBootstrapTokenCommand(),
newGetClusterSettingsCommand(),
newGetServiceAccountCommand(),
)
return command
}
+3 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/cirruslabs/orchard/internal/command/pause"
"github.com/cirruslabs/orchard/internal/command/portforward"
"github.com/cirruslabs/orchard/internal/command/resume"
"github.com/cirruslabs/orchard/internal/command/set"
"github.com/cirruslabs/orchard/internal/command/ssh"
"github.com/cirruslabs/orchard/internal/command/vnc"
"github.com/cirruslabs/orchard/internal/command/worker"
@@ -29,15 +30,16 @@ func NewRootCmd() *cobra.Command {
addGroupedCommands(command, "Working With Resources:",
create.NewCommand(),
deletepkg.NewCommand(),
get.NewCommand(),
list.NewCommand(),
logs.NewCommand(),
pause.NewCommand(),
portforward.NewCommand(),
resume.NewCommand(),
set.NewCommand(),
ssh.NewCommand(),
vnc.NewCommand(),
deletepkg.NewCommand(),
)
addGroupedCommands(command, "Administrative Tasks:",
+67
View File
@@ -0,0 +1,67 @@
package set
import (
"errors"
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/spf13/cobra"
)
var ErrClusterSettingsFailed = errors.New("failed to set cluster settings")
var hostDirPoliciesRaw []string
const hostDirPoliciesFlag = "host-dir-policies"
func newSetClusterSettingsCommand() *cobra.Command {
command := &cobra.Command{
Use: "cluster-settings",
Short: "Set cluster settings",
RunE: runSetClusterSettings,
}
command.PersistentFlags().StringSliceVar(&hostDirPoliciesRaw, hostDirPoliciesFlag, []string{},
fmt.Sprintf("comma-separated list of hostDir policies containing an allowed path prefix "+
"and an optional \":ro\" modifier to only allow read-only mounts for that path prefix "+
"(for example, --%s=/Users/ci/sources:ro,/tmp)", hostDirPoliciesFlag))
return command
}
func runSetClusterSettings(cmd *cobra.Command, args []string) error {
// Convert arguments
var hostDirPolicies []v1.HostDirPolicy
for _, hostDirPolicyRaw := range hostDirPoliciesRaw {
hostDirPolicy, err := v1.NewHostDirPolicyFromString(hostDirPolicyRaw)
if err != nil {
return err
}
hostDirPolicies = append(hostDirPolicies, hostDirPolicy)
}
// Check if we need to update anything in the cluster settings
if !cmd.Flag(hostDirPoliciesFlag).Changed {
return fmt.Errorf("%w: you need to specify at least one setting to update",
ErrClusterSettingsFailed)
}
// Update cluster settings
client, err := client.New()
if err != nil {
return err
}
clusterSettings, err := client.ClusterSettings().Get(cmd.Context())
if err != nil {
return err
}
if cmd.Flag(hostDirPoliciesFlag).Changed {
clusterSettings.HostDirPolicies = hostDirPolicies
}
return client.ClusterSettings().Set(cmd.Context(), clusterSettings)
}
+16
View File
@@ -0,0 +1,16 @@
package set
import (
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "set",
Short: "Set resource properties on the controller",
}
command.AddCommand(newSetClusterSettingsCommand())
return command
}
+8
View File
@@ -59,6 +59,14 @@ func (controller *Controller) initAPI() *gin.Engine {
controller.controllerInfo(c).Respond(c)
})
// Cluster settings
v1.GET("/cluster-settings", func(c *gin.Context) {
controller.getClusterSettings(c).Respond(c)
})
v1.PUT("/cluster-settings", func(c *gin.Context) {
controller.updateClusterSettings(c).Respond(c)
})
// Service accounts
v1.POST("/service-accounts", func(c *gin.Context) {
controller.createServiceAccount(c).Respond(c)
@@ -0,0 +1,53 @@
package controller
import (
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/internal/responder"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"net/http"
)
func (controller *Controller) getClusterSettings(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleAdminRead); responder != nil {
return responder
}
return controller.storeView(func(txn storepkg.Transaction) responder.Responder {
clusterSettings, err := txn.GetClusterSettings()
if err != nil {
controller.logger.Errorf("failed to retrieve cluster settings: %v", err)
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, clusterSettings)
})
}
func (controller *Controller) updateClusterSettings(ctx *gin.Context) responder.Responder {
if responder := controller.authorize(ctx, v1.ServiceAccountRoleAdminWrite); responder != nil {
return responder
}
var clusterSettings v1.ClusterSettings
if err := ctx.ShouldBindJSON(&clusterSettings); err != nil {
return responder.JSON(http.StatusBadRequest, NewErrorResponse("invalid JSON was provided"))
}
// Validate
for _, allowedHostDir := range clusterSettings.HostDirPolicies {
if allowedHostDir.PathPrefix == "" {
return responder.JSON(http.StatusBadRequest,
NewErrorResponse("pathPrefix field cannot be empty"))
}
}
return controller.storeUpdate(func(txn storepkg.Transaction) responder.Responder {
if err := txn.SetClusterSettings(clusterSettings); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &clusterSettings)
})
}
+46
View File
@@ -7,6 +7,7 @@ import (
"github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/samber/lo"
"net/http"
"time"
)
@@ -59,6 +60,11 @@ func (controller *Controller) createVM(ctx *gin.Context) responder.Responder {
vm.RestartPolicy = v1.RestartPolicyNever
}
// Validate hostDirs
if responder := controller.validateHostDirs(vm.HostDirs); responder != nil {
return responder
}
response := controller.storeUpdate(func(txn storepkg.Transaction) responder.Responder {
// Does the VM resource with this name already exists?
_, err := txn.GetVM(vm.Name)
@@ -220,3 +226,43 @@ func (controller *Controller) listVMEvents(ctx *gin.Context) responder.Responder
return responder.JSON(http.StatusOK, events)
})
}
func (controller *Controller) validateHostDirs(hostDirs []v1.HostDir) responder.Responder {
if len(hostDirs) == 0 {
return nil
}
// Retrieve cluster settings
var clusterSettings *v1.ClusterSettings
var err error
err = controller.store.View(func(txn storepkg.Transaction) error {
clusterSettings, err = txn.GetClusterSettings()
return err
})
if err != nil {
return responder.Code(http.StatusInternalServerError)
}
for _, hostDir := range hostDirs {
if hostDir.Name == "" {
return responder.JSON(http.StatusBadRequest,
NewErrorResponse("hostDir volume's \"name\" field cannot be empty"))
}
if hostDir.Path == "" {
return responder.JSON(http.StatusBadRequest,
NewErrorResponse("hostDir volume's \"path\" field cannot be empty"))
}
if !lo.SomeBy(clusterSettings.HostDirPolicies, func(hostDirPolicy v1.HostDirPolicy) bool {
return hostDirPolicy.Validate(hostDir.Path, hostDir.ReadOnly)
}) {
return responder.JSON(http.StatusBadRequest, NewErrorResponse("host directory %q is disallowed "+
"by policy, check your cluster settings", hostDir.String()))
}
}
return nil
}
+13
View File
@@ -113,6 +113,19 @@ func New(opts ...Option) (*Controller, error) {
ReadHeaderTimeout: 60 * time.Second,
}
// Ensure cluster settings object is present
if err := controller.store.Update(func(txn storepkg.Transaction) error {
_, err := txn.GetClusterSettings()
if errors.Is(err, storepkg.ErrNotFound) {
return txn.SetClusterSettings(v1.ClusterSettings{})
}
return err
}); err != nil {
return nil, fmt.Errorf("%w: failed to ensure cluster settings object is present: %v",
ErrInitFailed, err)
}
return controller, nil
}
@@ -0,0 +1,46 @@
package badger
import (
"encoding/json"
"github.com/cirruslabs/orchard/pkg/resource/v1"
)
var ClusterSettingsKey = []byte("/cluster-settings")
func (txn *Transaction) GetClusterSettings() (result *v1.ClusterSettings, err error) {
defer func() {
err = mapErr(err)
}()
item, err := txn.badgerTxn.Get(ClusterSettingsKey)
if err != nil {
return nil, err
}
valueBytes, err := item.ValueCopy(nil)
if err != nil {
return nil, err
}
var clusterSettings v1.ClusterSettings
err = json.Unmarshal(valueBytes, &clusterSettings)
if err != nil {
return nil, err
}
return &clusterSettings, nil
}
func (txn *Transaction) SetClusterSettings(clusterSettings v1.ClusterSettings) (err error) {
defer func() {
err = mapErr(err)
}()
valueBytes, err := json.Marshal(clusterSettings)
if err != nil {
return err
}
return txn.badgerTxn.Set(ClusterSettingsKey, valueBytes)
}
+3
View File
@@ -26,4 +26,7 @@ type Transaction interface {
AppendEvents(event []v1.Event, scope ...string) (err error)
ListEvents(scope ...string) (result []v1.Event, err error)
DeleteEvents(scope ...string) (err error)
GetClusterSettings() (*v1.ClusterSettings, error)
SetClusterSettings(clusterSettings v1.ClusterSettings) error
}
+109
View File
@@ -3,6 +3,7 @@ package tests_test
import (
"context"
"errors"
"fmt"
"github.com/cirruslabs/orchard/internal/command/dev"
"github.com/cirruslabs/orchard/internal/controller"
"github.com/cirruslabs/orchard/internal/worker"
@@ -18,6 +19,8 @@ import (
"golang.org/x/exp/slices"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
@@ -422,6 +425,112 @@ func TestVMGarbageCollection(t *testing.T) {
}), "failed to wait for the VM %s to be garbage-collected", vmName)
}
func TestHostDirs(t *testing.T) {
devClient := StartIntegrationTestEnvironment(t)
dirToMount := t.TempDir()
vmName := "test-host-dirs-" + uuid.NewString()
err := devClient.ClusterSettings().Set(context.Background(), &v1.ClusterSettings{
HostDirPolicies: []v1.HostDirPolicy{{PathPrefix: dirToMount}},
})
require.NoError(t, err)
scriptContent, err := os.ReadFile(filepath.Join("testdata", "host-dirs.sh"))
require.NoError(t, err)
err = devClient.VMs().Create(context.Background(), &v1.VM{
Meta: v1.Meta{
Name: vmName,
},
Image: "ghcr.io/cirruslabs/macos-ventura-base:latest",
CPU: 4,
Memory: 8 * 1024,
Headless: true,
Status: v1.VMStatusPending,
StartupScript: &v1.VMScript{
ScriptContent: string(scriptContent),
},
HostDirs: []v1.HostDir{
{Name: "readwrite", Path: dirToMount},
{Name: "readonly", Path: dirToMount, ReadOnly: true},
},
})
require.NoError(t, err)
var vm *v1.VM
require.True(t, Wait(2*time.Minute, func() bool {
vm, err = devClient.VMs().Get(context.Background(), vmName)
require.NoError(t, err)
t.Logf("Waiting for the VM to start. Current status: %s", vm.Status)
return vm.Status == v1.VMStatusRunning || vm.Status == v1.VMStatusFailed
}), "failed to start a VM")
require.Empty(t, vm.StatusMessage)
require.Equal(t, v1.VMStatusRunning, vm.Status)
var logLines []string
require.True(t, Wait(2*time.Minute, func() bool {
logLines, err = devClient.VMs().Logs(context.Background(), vmName)
require.NoError(t, err)
return len(logLines) > 0
}), "failed to wait for logs to become available")
fmt.Println(logLines)
require.EqualValues(t, []string{
"Read-write mount exists",
"Read-only mount exists",
"Failed to create a file in read-only mount",
"Successfully created a file in read-write mount",
}, logLines)
require.FileExists(t, filepath.Join(dirToMount, "test-rw.txt"))
require.NoFileExists(t, filepath.Join(dirToMount, "test-ro.txt"))
}
func TestHostDirsInvalidPolicy(t *testing.T) {
devClient := StartIntegrationTestEnvironment(t)
dirToMount := t.TempDir()
vmName := "test-host-dirs-" + uuid.NewString()
// Create a VM without creating any directory policies
// and make sure we get an error
vmSpec := &v1.VM{
Meta: v1.Meta{
Name: vmName,
},
Image: "ghcr.io/cirruslabs/macos-ventura-base:latest",
CPU: 4,
Memory: 8 * 1024,
Headless: true,
Status: v1.VMStatusPending,
HostDirs: []v1.HostDir{
{Name: "test" + uuid.NewString(), Path: dirToMount},
},
}
err := devClient.VMs().Create(context.Background(), vmSpec)
require.Error(t, err)
// Create a policy for our directory, but do not allow for writing
err = devClient.ClusterSettings().Set(context.Background(), &v1.ClusterSettings{
HostDirPolicies: []v1.HostDirPolicy{{PathPrefix: dirToMount, ReadOnly: true}},
})
require.NoError(t, err)
// Make sure we get error with the same spec
err = devClient.VMs().Create(context.Background(), vmSpec)
require.Error(t, err)
}
func hasVM(t *testing.T, name string, logger *zap.Logger) bool {
return hasVMByPredicate(t, func(vmInfo tart.VMInfo) bool {
return vmInfo.Name == name
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
if test -d "/Volumes/My Shared Files/readwrite"
then
echo "Read-write mount exists"
else
echo "Read-write mount not found"
exit 1
fi
if test -d "/Volumes/My Shared Files/readonly"
then
echo "Read-only mount exists"
else
echo "Read-only mount not found"
exit 1
fi
touch "/Volumes/My Shared Files/readonly/test-ro.txt" 2>/dev/null || echo "Failed to create a file in read-only mount"
touch "/Volumes/My Shared Files/readwrite/test-rw.txt" && echo "Successfully created a file in read-write mount"
+4
View File
@@ -147,6 +147,10 @@ func (vm *VM) run(ctx context.Context) error {
runArgs = append(runArgs, "--no-graphics")
}
for _, hostDir := range vm.Resource.HostDirs {
runArgs = append(runArgs, fmt.Sprintf("--dir=%s", hostDir.String()))
}
runArgs = append(runArgs, vm.id())
_, _, err := tart.Tart(ctx, vm.logger, runArgs...)
if err != nil {
+6
View File
@@ -335,3 +335,9 @@ func (client *Client) Controller() *ControllerService {
client: client,
}
}
func (client *Client) ClusterSettings() *ClusterSettingsService {
return &ClusterSettingsService{
client: client,
}
}
+26
View File
@@ -0,0 +1,26 @@
package client
import (
"context"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"net/http"
)
type ClusterSettingsService struct {
client *Client
}
func (service *ClusterSettingsService) Get(ctx context.Context) (*v1.ClusterSettings, error) {
var clusterSettings v1.ClusterSettings
err := service.client.request(ctx, http.MethodGet, "cluster-settings", nil, &clusterSettings, nil)
if err != nil {
return nil, err
}
return &clusterSettings, nil
}
func (service *ClusterSettingsService) Set(ctx context.Context, clusterSettings *v1.ClusterSettings) error {
return service.client.request(ctx, http.MethodPut, "cluster-settings", clusterSettings, nil, nil)
}
+5
View File
@@ -0,0 +1,5 @@
package v1
type ClusterSettings struct {
HostDirPolicies []HostDirPolicy `json:"hostDirPolicies"`
}
+58
View File
@@ -0,0 +1,58 @@
package v1
import (
"errors"
"fmt"
"strings"
)
var ErrInvalidHostDir = errors.New("invalid hostDir specification")
type HostDir struct {
Name string `json:"name"`
Path string `json:"path"`
ReadOnly bool `json:"ro"`
}
func NewHostDirFromString(s string) (HostDir, error) {
parts := strings.Split(s, ":")
if len(parts) > 3 {
return HostDir{}, fmt.Errorf("%w: hostDir specification can only contain 3 parts at max",
ErrInvalidHostDir)
}
if parts[0] == "" {
return HostDir{}, fmt.Errorf("%w: name cannot be empty", ErrInvalidHostDir)
}
if parts[1] == "" {
return HostDir{}, fmt.Errorf("%w: path cannot be empty", ErrInvalidHostDir)
}
var readOnly bool
if len(parts) == 3 {
if parts[2] == "ro" {
readOnly = true
} else {
return HostDir{}, fmt.Errorf("%w: hostDir's third part can only be \"ro\", got %q",
ErrInvalidHostDir, parts[2])
}
}
return HostDir{
Name: parts[0],
Path: parts[1],
ReadOnly: readOnly,
}, nil
}
func (hostDir HostDir) String() string {
var roPart string
if hostDir.ReadOnly {
roPart = ":ro"
}
return fmt.Sprintf("%s:%s%s", hostDir.Name, hostDir.Path, roPart)
}
+68
View File
@@ -0,0 +1,68 @@
package v1
import (
"errors"
"fmt"
"strings"
)
var ErrInvalidHostDirPolicy = errors.New("invalid hostDir policy")
type HostDirPolicy struct {
PathPrefix string `json:"pathPrefix"`
ReadOnly bool `json:"ro"`
}
func NewHostDirPolicyFromString(s string) (HostDirPolicy, error) {
parts := strings.Split(s, ":")
if len(parts) > 2 {
return HostDirPolicy{}, fmt.Errorf("%w: hostDir policy should contain 2 parts at max, found %d",
ErrInvalidHostDirPolicy, len(parts))
}
if parts[0] == "" {
return HostDirPolicy{}, fmt.Errorf("%w: path prefix cannot be empty", ErrInvalidHostDirPolicy)
}
var readOnly bool
if len(parts) == 2 {
if parts[1] == "ro" {
readOnly = true
} else {
return HostDirPolicy{}, fmt.Errorf("%w: hostDir policy's second part can only be \"ro\", found %q",
ErrInvalidHostDirPolicy, parts[1])
}
}
return HostDirPolicy{
PathPrefix: parts[0],
ReadOnly: readOnly,
}, nil
}
func (policy HostDirPolicy) Validate(path string, readOnly bool) bool {
if strings.Contains(path, "..") {
return false
}
if policy.ReadOnly && !readOnly {
return false
}
return strings.HasPrefix(
strings.TrimSuffix(path, "/"),
strings.TrimSuffix(policy.PathPrefix, "/"),
)
}
func (policy HostDirPolicy) String() string {
var roPart string
if policy.ReadOnly {
roPart = ":ro"
}
return fmt.Sprintf("%s%s", policy.PathPrefix, roPart)
}
+63
View File
@@ -0,0 +1,63 @@
package v1_test
import (
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/stretchr/testify/require"
"testing"
)
func TestNewHostDirPolicyFromString(t *testing.T) {
policy, err := v1.NewHostDirPolicyFromString("/Users/ci/src:ro")
require.NoError(t, err)
require.EqualValues(t, v1.HostDirPolicy{
PathPrefix: "/Users/ci/src",
ReadOnly: true,
}, policy)
_, err = v1.NewHostDirPolicyFromString("/Users/ci/src:ro:something")
require.Error(t, err)
_, err = v1.NewHostDirPolicyFromString("/Users/ci/src:rw")
require.Error(t, err)
}
func TestHostDirPolicyValidate(t *testing.T) {
policy := &v1.HostDirPolicy{PathPrefix: "/Users/ci/src"}
// Valid uses
require.True(t, policy.Validate("/Users/ci/src", true))
require.True(t, policy.Validate("/Users/ci/src/", true))
require.True(t, policy.Validate("/Users/ci/src/website", true))
// Invalid uses
require.False(t, policy.Validate("/Users/ci/", true))
require.False(t, policy.Validate("/Users", true))
require.False(t, policy.Validate("/tmp", true))
require.False(t, policy.Validate("/", true))
// No path traversal, even within the path prefix
require.False(t, policy.Validate("/Users/ci/src/website/../../../../../../etc/passwd", true))
require.False(t, policy.Validate("/Users/ci/src/website/..", true))
require.False(t, policy.Validate("/Users/ci/src/..", true))
require.False(t, policy.Validate("/Users/ci/..", true))
require.False(t, policy.Validate("/Users/..", true))
require.False(t, policy.Validate("/..", true))
}
func TestHostDirPolicyValidateReadOnly(t *testing.T) {
policy := &v1.HostDirPolicy{PathPrefix: "/Users/ci/src", ReadOnly: true}
const desiredPath = "/Users/ci/src/website"
// Only read-only is allowed
require.True(t, policy.Validate(desiredPath, true))
require.False(t, policy.Validate(desiredPath, false))
}
func TestHostDirPolicyString(t *testing.T) {
policyRw := &v1.HostDirPolicy{PathPrefix: "/Users/ci/src"}
require.EqualValues(t, "/Users/ci/src", policyRw.String())
policyRo := &v1.HostDirPolicy{PathPrefix: "/Users/ci/src", ReadOnly: true}
require.EqualValues(t, "/Users/ci/src:ro", policyRo.String())
}
+25
View File
@@ -0,0 +1,25 @@
package v1_test
import (
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/stretchr/testify/require"
"testing"
)
func TestNewHostDirFromString(t *testing.T) {
hostDir, err := v1.NewHostDirFromString("large-project:/Users/ci/src/www:ro")
require.NoError(t, err)
require.EqualValues(t, v1.HostDir{
Name: "large-project",
Path: "/Users/ci/src/www",
ReadOnly: true,
}, hostDir)
}
func TestHostDirString(t *testing.T) {
require.EqualValues(t, "large-project:/Users/ci/src/www:ro", v1.HostDir{
Name: "large-project",
Path: "/Users/ci/src/www",
ReadOnly: true,
}.String())
}
+3
View File
@@ -49,6 +49,9 @@ type VM struct {
// Resources required by this VM.
Resources Resources `json:"resources"`
// HostDir is a list of host directories to be mounted to the VM.
HostDirs []HostDir `json:"hostDirs"`
Meta
}