diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2c861a7..73ce4cd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,9 @@ jobs: uses: actions/setup-go@v4 with: go-version: '1.21' + - + name: Symlink go + run: sudo ln -f -s /opt/hostedtoolcache/go/1.21.5/x64/bin/* /usr/bin/ - name: Install ginkgo shell: bash --noprofile --norc -x -eo pipefail {0} @@ -60,21 +63,13 @@ jobs: ~/.local/share/pnpm/pnpm install ~/.local/share/pnpm/pnpm build - - name: Test agent - working-directory: ./agent - run: go test -v ./... + name: Run spec suite + working-directory: . + run: sudo -E ~/go/bin/ginkgo run -r --randomize-all --vv -race --trace --keep-going ./spec - - name: Test nex cli - working-directory: ./nex - run: go test -v ./... - - - name: Test control-api - working-directory: ./internal/control-api - run: ginkgo - - - name: Test nex node - working-directory: ./internal/node - run: go test -v ./... + name: Run test suite + working-directory: . + run: go test -v -race ./test build: runs-on: ubuntu-latest @@ -102,7 +97,6 @@ jobs: working-directory: ./nex run: go build . - goreleaser: runs-on: ubuntu-latest steps: diff --git a/Taskfile.yml b/Taskfile.yml index 0350be9d..7c34d5de 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -16,6 +16,16 @@ tasks: cmds: - go build -tags netgo -ldflags '-extldflags "-static"' + clean: + cmds: + - rm -f nex/nex + - sudo rm -rf /opt/cni/bin/* + - sudo rm -rf /var/lib/cni/* + - sudo rm -rf /etc/cni/conf.d/* + - sudo rm -f /tmp/rootfs-* + - sudo rm -f /tmp/.firecracker.* + - sudo rm -rf /tmp/pnats + nex: dir: nex sources: @@ -30,6 +40,12 @@ tasks: cmds: - go build -tags netgo -ldflags '-extldflags "-static"' + test: + deps: [clean] + cmds: + - sudo $GOPATH/bin/ginkgo run -r --randomize-all --vv -race --trace --keep-going ./spec #--cover --coverprofile=.coverage-report.out + - go test -v -race ./test + ui: dir: ui/web cmds: diff --git a/internal/control-api/client.go b/internal/control-api/client.go index 4ed8b708..88f8c8df 100644 --- a/internal/control-api/client.go +++ b/internal/control-api/client.go @@ -228,7 +228,7 @@ func handleLogEntry(api *Client, ch chan EmittedLog) func(m *nats.Msg) { if len(tokens) != 6 { return } - var logEntry rawLog + var logEntry RawLog err := json.Unmarshal(m.Data, &logEntry) if err != nil { api.log.WithError(err).Error("Log entry deserialization failure") @@ -243,7 +243,7 @@ func handleLogEntry(api *Client, ch chan EmittedLog) func(m *nats.Msg) { NodeId: tokens[3], Workload: tokens[4], Timestamp: time.Now().UTC().Format(time.RFC3339), - rawLog: logEntry, + RawLog: logEntry, } } diff --git a/internal/control-api/types.go b/internal/control-api/types.go index 02c074b9..dcced2a8 100644 --- a/internal/control-api/types.go +++ b/internal/control-api/types.go @@ -77,10 +77,10 @@ type EmittedLog struct { NodeId string `json:"node_id"` Workload string `json:"workload_id"` Timestamp string `json:"timestamp"` - rawLog + RawLog } -type rawLog struct { +type RawLog struct { Text string `json:"text"` Level logrus.Level `json:"level"` MachineId string `json:"machine_id"` diff --git a/internal/models/cli.go b/internal/models/cli.go index 2b37b263..2ad8c960 100644 --- a/internal/models/cli.go +++ b/internal/models/cli.go @@ -76,23 +76,23 @@ type WatchOptions struct { // Node configuration is used to configure the node process as well // as the virtual machines it produces type NodeOptions struct { - Config string `json:"-"` + ConfigFilepath string `json:"-"` ForceDepInstall bool `json:"-"` - KernelFile string `json:"kernel_file"` - RootFsFile string `json:"rootfs_file"` - DefaultDir string `json:"default_resource_dir"` - CNI CNIDefinition `json:"cni"` - InternalNodeHost *string `json:"internal_node_host,omitempty"` - InternalNodePort *int `json:"internal_node_port"` - KernelPath *string `json:"kernel_path"` - MachinePoolSize int `json:"machine_pool_size"` - MachineTemplate MachineTemplate `json:"machine_template"` - RateLimiters *Limiters `json:"rate_limiters,omitempty"` - RootFsPath *string `json:"rootfs_path"` - ValidIssuers []string `json:"valid_issuers,omitempty"` - WorkloadTypes []string `json:"workload_types,omitempty"` - Tags map[string]string `json:"tags,omitempty"` + CNI CNIDefinition `json:"cni"` + DefaultResourceDir string `json:"default_resource_dir"` + InternalNodeHost *string `json:"internal_node_host,omitempty"` + InternalNodePort *int `json:"internal_node_port"` + KernelFilepath string `json:"kernel_file"` + KernelPath *string `json:"kernel_path"` // FIXME-- audit json + MachinePoolSize int `json:"machine_pool_size"` + MachineTemplate MachineTemplate `json:"machine_template"` + RateLimiters *Limiters `json:"rate_limiters,omitempty"` + RootFsFilepath string `json:"rootfs_file"` // FIXME-- audit json + RootFsPath *string `json:"rootfs_path"` + Tags map[string]string `json:"tags,omitempty"` + ValidIssuers []string `json:"valid_issuers,omitempty"` + WorkloadTypes []string `json:"workload_types,omitempty"` Errors []error `json:"errors,omitempty"` } @@ -125,7 +125,6 @@ type MachineTemplate struct { } type TokenBucket struct { - // The initial size of a token bucket. // Minimum: 0 OneTimeBurst *int64 `json:"one_time_burst,omitempty"` diff --git a/internal/node/config.go b/internal/node/config.go index 6f0f1b67..b54a691b 100644 --- a/internal/node/config.go +++ b/internal/node/config.go @@ -22,25 +22,56 @@ var ( defaultWorkloadTypes = []string{"elf", "v8", "wasm"} ) +type CliOptions struct { + // Servers is the list of servers to connect to + Servers string + // Creds is nats credentials to authenticate with + Creds string + // TlsCert is the TLS Public Certificate + TlsCert string + // TlsKey is the TLS Private Key + TlsKey string + // TlsCA is the certificate authority to verify the connection with + TlsCA string + // Timeout is how long to wait for operations + Timeout time.Duration + // ConnectionName is the name to use for the underlying NATS connection + ConnectionName string + // Username is the username or token to connect with + Username string + // Password is the password to connect with + Password string + // Nkey is the file holding a nkey to connect with + Nkey string + // TlsFirst configures theTLSHandshakeFirst behavior in nats.go + TlsFirst bool + // Path to the file containing virtual machine management settings and node-wide settings + NodeConfigFile string + // When enabled, the nex node will not clean up logs or rootfs files + ForensicMode bool + // When enabled, preflight will automatically install or reinstall dependencies + ForceDependencyInstall bool +} + // Node configuration is used to configure the node process as well // as the virtual machines it produces type NodeConfiguration struct { - KernelFile string `json:"kernel_file"` - RootFsFile string `json:"rootfs_file"` - DefaultDir string `json:"default_resource_dir"` - CNI CNIDefinition `json:"cni"` - InternalNodeHost *string `json:"internal_node_host,omitempty"` - InternalNodePort *int `json:"internal_node_port"` - KernelPath *string `json:"kernel_path"` - MachinePoolSize int `json:"machine_pool_size"` - MachineTemplate MachineTemplate `json:"machine_template"` - RateLimiters *Limiters `json:"rate_limiters,omitempty"` - RootFsPath *string `json:"rootfs_path"` - ValidIssuers []string `json:"valid_issuers,omitempty"` - WorkloadTypes []string `json:"workload_types,omitempty"` - Tags map[string]string `json:"tags,omitempty"` - ForensicMode bool `json:"-"` - ForceDepInstall bool `json:"-"` + KernelFile string `json:"kernel_file"` + RootFsFile string `json:"rootfs_file"` + DefaultResourceDir string `json:"default_resource_dir"` + CNI CNIDefinition `json:"cni"` + InternalNodeHost *string `json:"internal_node_host,omitempty"` + InternalNodePort *int `json:"internal_node_port"` + KernelPath *string `json:"kernel_path"` // FIXME + MachinePoolSize int `json:"machine_pool_size"` + MachineTemplate MachineTemplate `json:"machine_template"` + RateLimiters *Limiters `json:"rate_limiters,omitempty"` + RootFsPath *string `json:"rootfs_path"` // FIXME + ValidIssuers []string `json:"valid_issuers,omitempty"` + WorkloadTypes []string `json:"workload_types,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + ForensicMode bool `json:"-"` + ForceDepInstall bool `json:"-"` Errors []error `json:"errors,omitempty"` } @@ -48,7 +79,13 @@ type NodeConfiguration struct { func (c *NodeConfiguration) Validate() bool { c.Errors = make([]error, 0) - // TODO-- add validation + if _, err := os.Stat(c.KernelFile); errors.Is(err, os.ErrNotExist) { + c.Errors = append(c.Errors, err) + } + + if _, err := os.Stat(c.RootFsFile); errors.Is(err, os.ErrNotExist) { + c.Errors = append(c.Errors, err) + } return len(c.Errors) == 0 } @@ -62,8 +99,8 @@ type Limiters struct { // Defines a reference to the CNI network name, which is defined and configured in a {network}.conflist file, as per // CNI convention type CNIDefinition struct { - NetworkName *string `json:"network_name"` InterfaceName *string `json:"interface_name"` + NetworkName *string `json:"network_name"` } // Defines the CPU and memory usage of a machine to be configured when it is added to the pool @@ -73,7 +110,6 @@ type MachineTemplate struct { } type TokenBucket struct { - // The initial size of a token bucket. // Minimum: 0 OneTimeBurst *int64 `json:"one_time_burst,omitempty"` @@ -114,65 +150,38 @@ func DefaultNodeConfiguration() NodeConfiguration { } } -// Retrieves the node configuration from the specified file +// Reads the node configuration from the specified configuration file path func LoadNodeConfiguration(configFilePath string) (*NodeConfiguration, error) { bytes, err := os.ReadFile(configFilePath) if err != nil { return nil, err } + config := DefaultNodeConfiguration() err = json.Unmarshal(bytes, &config) - if len(config.WorkloadTypes) == 0 { - config.WorkloadTypes = defaultWorkloadTypes - } if err != nil { return nil, err } - if config.KernelFile == "" && config.DefaultDir != "" { - config.KernelFile = filepath.Join(config.DefaultDir, "vmlinux") - } else if config.KernelFile == "" && config.DefaultDir == "" { + if len(config.WorkloadTypes) == 0 { + config.WorkloadTypes = defaultWorkloadTypes + } + + if config.KernelFile == "" && config.DefaultResourceDir != "" { + config.KernelFile = filepath.Join(config.DefaultResourceDir, "vmlinux") + } else if config.KernelFile == "" && config.DefaultResourceDir == "" { return nil, errors.New("invalid kernel file setting") } - if config.RootFsFile == "" && config.DefaultDir != "" { - config.RootFsFile = filepath.Join(config.DefaultDir, "rootfs.ext4") - } else if config.KernelFile == "" && config.DefaultDir == "" { + + if config.RootFsFile == "" && config.DefaultResourceDir != "" { + config.RootFsFile = filepath.Join(config.DefaultResourceDir, "rootfs.ext4") + } else if config.KernelFile == "" && config.DefaultResourceDir == "" { return nil, errors.New("invalid rootfs file setting") } if config.Tags == nil { config.Tags = make(map[string]string) } - return &config, nil -} -type CliOptions struct { - // Servers is the list of servers to connect to - Servers string - // Creds is nats credentials to authenticate with - Creds string - // TlsCert is the TLS Public Certificate - TlsCert string - // TlsKey is the TLS Private Key - TlsKey string - // TlsCA is the certificate authority to verify the connection with - TlsCA string - // Timeout is how long to wait for operations - Timeout time.Duration - // ConnectionName is the name to use for the underlying NATS connection - ConnectionName string - // Username is the username or token to connect with - Username string - // Password is the password to connect with - Password string - // Nkey is the file holding a nkey to connect with - Nkey string - // TlsFirst configures theTLSHandshakeFirst behavior in nats.go - TlsFirst bool - // Path to the file containing virtual machine management settings and node-wide settings - NodeConfigFile string - // When enabled, the nex node will not clean up logs or rootfs files - ForensicMode bool - // When enabled, preflight will automatically install or reinstall dependencies - ForceDependencyInstall bool + return &config, nil } diff --git a/internal/node/init.go b/internal/node/init.go index 0c654d8d..8575a792 100644 --- a/internal/node/init.go +++ b/internal/node/init.go @@ -19,30 +19,30 @@ import ( func CmdUp(opts *nexmodels.Options, nodeopts *nexmodels.NodeOptions, ctx context.Context, cancel context.CancelFunc, log *logrus.Logger) { nc, err := generateConnectionFromOpts(opts) if err != nil { - log.WithError(err).Error("Failed to connect to NATS") - os.Exit(1) + log.WithError(err).Error("Failed to connect to NATS server") + panic("failed to connect to NATS server") } log.Infof("Established node NATS connection to: %s", opts.Servers) - config, err := LoadNodeConfiguration(nodeopts.Config) + config, err := LoadNodeConfiguration(nodeopts.ConfigFilepath) if err != nil { - log.WithError(err).WithField("file", nodeopts.Config).Error("Failed to load node configuration file") - os.Exit(1) + log.WithError(err).WithField("file", nodeopts.ConfigFilepath).Error("Failed to load node configuration file") + panic("failed to load node configuration file") } - log.Infof("Loaded node configuration from '%s'", nodeopts.Config) + log.Infof("Loaded node configuration from '%s'", nodeopts.ConfigFilepath) manager, err := NewMachineManager(ctx, cancel, nc, config, log) if err != nil { log.WithError(err).Error("Failed to initialize machine manager") - os.Exit(1) + panic("failed to initialize machine manager") } err = manager.Start() if err != nil { log.WithError(err).Error("Failed to start machine manager") - os.Exit(1) + panic("failed to start machine manager") } setupSignalHandlers(log, manager) @@ -51,7 +51,7 @@ func CmdUp(opts *nexmodels.Options, nodeopts *nexmodels.NodeOptions, ctx context err = api.Start() if err != nil { log.WithError(err).Error("Failed to start API listener") - os.Exit(1) + panic("failed to start API listener") } } @@ -93,6 +93,7 @@ func generateConnectionFromOpts(opts *nexmodels.Options) (*nats.Conn, error) { if err != nil { return nil, err } + return conn, nil } @@ -111,34 +112,32 @@ func setupSignalHandlers(log *logrus.Logger, manager *MachineManager) { if err != nil { log.WithError(err).Warn("Machine manager failed to stop") } - ClearMyApiSockets() - os.Exit(0) + cleanSockets() + os.Exit(0) // FIXME case s == syscall.SIGQUIT: log.Infof("Caught quit signal: %s, still trying graceful shutdown", s.String()) err := manager.Stop() if err != nil { log.WithError(err).Warn("Machine manager failed to stop") } - ClearMyApiSockets() - os.Exit(0) + cleanSockets() + os.Exit(0) // FIXME } } }() } func CmdPreflight(opts *nexmodels.Options, nodeopts *nexmodels.NodeOptions, ctx context.Context, cancel context.CancelFunc, log *logrus.Logger) { - config, err := LoadNodeConfiguration(nodeopts.Config) + config, err := LoadNodeConfiguration(nodeopts.ConfigFilepath) if err != nil { - fmt.Printf("Failed to load configuration file: %s\n", err) - os.Exit(1) + panic(fmt.Errorf("failed to load configuration file: %s", err)) } config.ForceDepInstall = nodeopts.ForceDepInstall err = CheckPreRequisites(config) if err != nil { - fmt.Printf("Preflight checks failed: %s\n", err) - os.Exit(1) + panic(fmt.Errorf("preflight checks failed: %s", err)) } } @@ -146,7 +145,7 @@ func CmdPreflight(opts *nexmodels.Options, nodeopts *nexmodels.NodeOptions, ctx // to ensure we get the full IP range // Remove firecracker VM sockets created by this pid -func ClearMyApiSockets() { +func cleanSockets() { dir, err := os.ReadDir(os.TempDir()) if err != nil { logrus.WithError(err).Error("Failed to read temp directory") diff --git a/internal/node/running_vm.go b/internal/node/running_vm.go index 30a44ce8..828eeb91 100644 --- a/internal/node/running_vm.go +++ b/internal/node/running_vm.go @@ -35,7 +35,6 @@ type runningFirecracker struct { } func (vm *runningFirecracker) shutDown() { - log.WithField("vmid", vm.vmmID). WithField("ip", vm.ip). Info("Machine stopping") @@ -209,8 +208,8 @@ func generateFirecrackerConfig(id string, config *NodeConfiguration) (firecracke AllowMMDS: true, // Use CNI to get dynamic IP CNIConfiguration: &firecracker.CNIConfiguration{ - NetworkName: *config.CNI.NetworkName, IfName: *config.CNI.InterfaceName, + NetworkName: *config.CNI.NetworkName, }, //OutRateLimiter: firecracker.NewRateLimiter(..., ...), //InRateLimiter: firecracker.NewRateLimiter(..., ...), diff --git a/nex/node_linux_amd64.go b/nex/node_linux_amd64.go index 7a50423e..a442c169 100644 --- a/nex/node_linux_amd64.go +++ b/nex/node_linux_amd64.go @@ -10,10 +10,10 @@ import ( func init() { node_up := nodes.Command("up", "Starts a NEX node") - node_up.Flag("config", "configuration file for the node").Default("./config.json").StringVar(&NodeOpts.Config) + node_up.Flag("config", "configuration file for the node").Default("./config.json").StringVar(&NodeOpts.ConfigFilepath) node_preflight := nodes.Command("preflight", "Checks system for node requirements and installs missing") node_preflight.Flag("force", "installs missing dependencies without prompt").Default("false").BoolVar(&NodeOpts.ForceDepInstall) - node_preflight.Flag("config", "configuration file for the node").Default("./config.json").StringVar(&NodeOpts.Config) + node_preflight.Flag("config", "configuration file for the node").Default("./config.json").StringVar(&NodeOpts.ConfigFilepath) node_up.Action(RunNodeUp) node_preflight.Action(RunNodePreflight) diff --git a/internal/control-api/monitor_test.go b/spec/monitor_test.go similarity index 83% rename from internal/control-api/monitor_test.go rename to spec/monitor_test.go index 9fbd541e..1db257ce 100644 --- a/internal/control-api/monitor_test.go +++ b/spec/monitor_test.go @@ -1,13 +1,16 @@ -package controlapi +package spec import ( "encoding/json" + "fmt" "time" cloudevents "github.com/cloudevents/sdk-go" "github.com/nats-io/nats.go" "github.com/sirupsen/logrus" + . "github.com/ConnectEverything/nex/internal/control-api" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -27,13 +30,13 @@ var _ = Describe("event monitor", func() { BeforeEach(func() { var err error - nc, err = nats.Connect(nats.DefaultURL) - Expect(err).ToNot(BeNil()) + nc, err = nats.Connect(fmt.Sprintf("nats://0.0.0.0:%d", *_fixtures.natsPort)) + Expect(err).To(BeNil()) log = logrus.New() client = NewApiClient(nc, time.Second, log) ch, err = client.MonitorEvents("*", "*", 0) - Expect(err).ToNot(BeNil()) + Expect(err).To(BeNil()) evt := cloudevents.NewEvent() evt.SetType("workload_started") @@ -83,20 +86,20 @@ var _ = Describe("log monitor", func() { var ch chan EmittedLog var subject EmittedLog - var raw rawLog // FIXME... + var raw RawLog // FIXME... BeforeEach(func() { var err error - nc, err = nats.Connect(nats.DefaultURL) - Expect(err).ToNot(BeNil()) + nc, err = nats.Connect(fmt.Sprintf("nats://0.0.0.0:%d", *_fixtures.natsPort)) + Expect(err).To(BeNil()) log = logrus.New() client = NewApiClient(nc, time.Second, log) ch, err = client.MonitorLogs("*", "*", "*", "*", 0) - Expect(err).ToNot(BeNil()) + Expect(err).To(BeNil()) - raw = rawLog{Text: "hey from test", Level: logrus.DebugLevel, MachineId: "vm1234"} + raw = RawLog{Text: "hey from test", Level: logrus.DebugLevel, MachineId: "vm1234"} bytes, _ := json.Marshal(raw) _ = nc.Publish("$NEX.logs.default.Nxxxx.echoservice.vm1234", bytes) @@ -120,6 +123,6 @@ var _ = Describe("log monitor", func() { }) It("wraps the raw on the wire log", func(ctx SpecContext) { - Expect(subject.rawLog).To(Equal(raw)) + Expect(subject.RawLog).To(Equal(raw)) }) }) diff --git a/spec/node_test.go b/spec/node_test.go new file mode 100644 index 00000000..afecf342 --- /dev/null +++ b/spec/node_test.go @@ -0,0 +1,148 @@ +package spec + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "sync" + + "github.com/sirupsen/logrus" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/ConnectEverything/nex/internal/models" + nexnode "github.com/ConnectEverything/nex/internal/node" +) + +const defaultCNIPluginBinPath = "/opt/cni/bin" +const defaultCNIConfigurationPath = "/etc/cni/conf.d" +const defaultFirecrackerBinPath = "/usr/local/bin/firecracker" + +var _ = Describe("nex node", func() { + var log *logrus.Logger + var ctxx context.Context + var cancel context.CancelFunc + + var opts *models.Options + var nodeOpts *models.NodeOptions + + var nodeConfig nexnode.NodeConfiguration + + var validResourceDir string // prevent downloading kernel and rootfs template multiple times + var validResourceDirOnce sync.Once + + BeforeEach(func() { + ctxx, cancel = context.WithCancel(context.Background()) + log = logrus.New() + + opts = &models.Options{ + Servers: _fixtures.natsServer.ClientURL(), + } + nodeOpts = &models.NodeOptions{} + + _ = os.MkdirAll(defaultCNIPluginBinPath, 0755) + _ = os.MkdirAll(defaultCNIConfigurationPath, 0755) + + validResourceDirOnce.Do(func() { + validResourceDir = filepath.Join(os.TempDir(), fmt.Sprintf("%d-spec-nex-wd", _fixtures.seededRand.Int())) + }) + }) + + Describe("preflight", func() { + Context("when the specified configuration file does not exist", func() { + BeforeEach(func() { + nodeOpts.ConfigFilepath = filepath.Join(os.TempDir(), fmt.Sprintf("%d-non-existent-nex-conf.json", _fixtures.seededRand.Int())) + }) + + It("should panic", func(ctx SpecContext) { + Expect(func() { + nexnode.CmdPreflight(opts, nodeOpts, ctxx, cancel, log) + }).To(PanicWith(fmt.Errorf("failed to load configuration file: open %s: no such file or directory", nodeOpts.ConfigFilepath))) + }) + }) + + Context("when the specified node configuration file exists", func() { + BeforeEach(func() { + nodeConfig = nexnode.DefaultNodeConfiguration() + nodeOpts.ConfigFilepath = path.Join(os.TempDir(), fmt.Sprintf("%d-spec-nex-conf.json", _fixtures.seededRand.Int())) + }) + + JustBeforeEach(func() { + cfg, _ := json.Marshal(nodeConfig) + _ = os.WriteFile(nodeOpts.ConfigFilepath, cfg, 0644) + }) + + AfterEach(func() { + os.Remove(nodeOpts.ConfigFilepath) + }) + + Describe("default dependency resolution", func() { + Context("when the node configuration specifies a default_resource_dir", func() { + Context("when the specified default_resource_dir does not exist on the host", func() { + BeforeEach(func() { + nodeConfig.DefaultResourceDir = filepath.Join(os.TempDir(), fmt.Sprintf("%d-non-existent-nex-resource-dir", _fixtures.seededRand.Int())) + }) + + It("should panic", func(ctx SpecContext) { + Expect(func() { + nexnode.CmdPreflight(opts, nodeOpts, ctxx, cancel, log) + }).To(PanicWith(errors.New("preflight checks failed: EOF"))) + }) + }) + + Context("when the specified default_resource_dir exists on the host", func() { + BeforeEach(func() { + nodeConfig.DefaultResourceDir = validResourceDir + _ = os.Mkdir(nodeConfig.DefaultResourceDir, 0755) + nodeOpts.ForceDepInstall = true + }) + + JustBeforeEach(func() { + nexnode.CmdPreflight(opts, nodeOpts, ctxx, cancel, log) + }) + + It("should install the host-local CNI plugin", func(ctx SpecContext) { + _, err := os.Stat(filepath.Join(defaultCNIPluginBinPath, "host-local")) + Expect(err).To(BeNil()) + }) + + It("should install the ptp CNI plugin", func(ctx SpecContext) { + _, err := os.Stat(filepath.Join(defaultCNIPluginBinPath, "ptp")) + Expect(err).To(BeNil()) + }) + + It("should install the tc-redirect-tap CNI plugin", func(ctx SpecContext) { + _, err := os.Stat(filepath.Join(defaultCNIPluginBinPath, "tc-redirect-tap")) + Expect(err).To(BeNil()) + }) + + It("should install the default firecracker binary", func(ctx SpecContext) { + _, err := os.Stat(defaultFirecrackerBinPath) + Expect(err).To(BeNil()) + }) + + It("should install the default CNI configuration", func(ctx SpecContext) { + _, err := os.Stat(filepath.Join(defaultCNIConfigurationPath, fmt.Sprintf("%s.conflist", *nodeConfig.CNI.NetworkName))) + Expect(err).To(BeNil()) + }) + + It("should fetch the default vmlinux kernel", func(ctx SpecContext) { + _, err := os.Stat(filepath.Join(nodeConfig.DefaultResourceDir, "vmlinux")) + Expect(err).To(BeNil()) + }) + + It("should fetch the default agent rootfs template", func(ctx SpecContext) { + _, err := os.Stat(filepath.Join(nodeConfig.DefaultResourceDir, "rootfs.ext4")) + Expect(err).To(BeNil()) + }) + }) + }) + }) + }) + }) +}) diff --git a/spec/spec_suite_test.go b/spec/spec_suite_test.go new file mode 100644 index 00000000..73fbc412 --- /dev/null +++ b/spec/spec_suite_test.go @@ -0,0 +1,96 @@ +package spec + +import ( + "fmt" + "math/rand" + "net/url" + "os" + "path" + "strconv" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" +) + +type testFixtures struct { + natsServer *server.Server + natsConn *nats.Conn + natsPort *int + natsStoreDir string + + seededRand *rand.Rand +} + +var _fixtures *testFixtures + +func TestSpec(t *testing.T) { + err := initFixtures() + if err != nil { + t.Errorf(fmt.Sprintf("failed to initialize fixtures; %s", err.Error())) + } + + defer cleanupFixtures() + + RegisterFailHandler(Fail) + RunSpecs(t, "Spec Suite") +} + +func initFixtures() error { + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + _fixtures = &testFixtures{ + natsStoreDir: path.Join(os.TempDir(), fmt.Sprintf("%d-spec", seededRand.Int())), + seededRand: seededRand, + } + + var err error + _fixtures.natsServer, _fixtures.natsConn, _fixtures.natsPort, err = startNATS(_fixtures.natsStoreDir) + if err != nil { + return err + } + + return nil +} + +func cleanupFixtures() { + _fixtures.natsConn.Close() + _fixtures.natsServer.Shutdown() + _fixtures.natsServer.WaitForShutdown() + + os.RemoveAll(_fixtures.natsStoreDir) +} + +func startNATS(storeDir string) (*server.Server, *nats.Conn, *int, error) { + ns, err := server.NewServer(&server.Options{ + Host: "0.0.0.0", + Port: -1, + JetStream: true, + NoLog: true, + StoreDir: storeDir, + }) + if err != nil { + return nil, nil, nil, err + } + ns.Start() + + clientURL, err := url.Parse(ns.ClientURL()) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to parse internal NATS client URL: %s", err) + } + + port, err := strconv.Atoi(clientURL.Port()) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to parse internal NATS client URL: %s", err) + } + + nc, err := nats.Connect(ns.ClientURL()) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to connect to NATS server: %s", err) + } + + return ns, nc, &port, nil +} diff --git a/internal/control-api/run_test.go b/test/run_test.go similarity index 93% rename from internal/control-api/run_test.go rename to test/run_test.go index dba3b67a..7609ee93 100644 --- a/internal/control-api/run_test.go +++ b/test/run_test.go @@ -1,8 +1,9 @@ -package controlapi +package test import ( "testing" + . "github.com/ConnectEverything/nex/internal/control-api" "github.com/nats-io/nkeys" ) diff --git a/internal/control-api/stop_test.go b/test/stop_test.go similarity index 96% rename from internal/control-api/stop_test.go rename to test/stop_test.go index 59fd58c9..bc427fc5 100644 --- a/internal/control-api/stop_test.go +++ b/test/stop_test.go @@ -1,10 +1,11 @@ -package controlapi +package test import ( "fmt" "testing" "time" + . "github.com/ConnectEverything/nex/internal/control-api" "github.com/nats-io/nkeys" ) diff --git a/agent/providers/lib/wasm_test.go b/test/wasm_test.go similarity index 86% rename from agent/providers/lib/wasm_test.go rename to test/wasm_test.go index 49a7b69c..0a3a2cb8 100644 --- a/agent/providers/lib/wasm_test.go +++ b/test/wasm_test.go @@ -1,15 +1,14 @@ -package lib +package test import ( "testing" + "github.com/ConnectEverything/nex/agent/providers/lib" agentapi "github.com/ConnectEverything/nex/internal/agent-api" ) func TestWasmExecution(t *testing.T) { - t.Skip("TODO") - - file := "../../../examples/wasm/echofunction/echofunction.wasm" + file := "../examples/wasm/echofunction/echofunction.wasm" typ := "wasm" params := &agentapi.ExecutionProviderParams{ DeployRequest: agentapi.DeployRequest{ @@ -34,7 +33,7 @@ func TestWasmExecution(t *testing.T) { NATSConn: nil, // FIXME } params.DeployRequest.WorkloadType = &typ - wasm, err := InitNexExecutionProviderWasm(params) + wasm, err := lib.InitNexExecutionProviderWasm(params) if err != nil { t.Fatalf("Failed to instantiate wasm provider: %s", err) }