Skip to content

Commit

Permalink
Review requests
Browse files Browse the repository at this point in the history
Signed-off-by: Pavel Abramov <[email protected]>
  • Loading branch information
uncleDecart committed Nov 6, 2024
1 parent 1be9d4e commit 6becead
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 205 deletions.
3 changes: 0 additions & 3 deletions cmd/edenSetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ func newSetupCmd(configName, verbosity *string) *cobra.Command {
Long: `Setup harness.`,
PersistentPreRunE: preRunViperLoadFunction(cfg, configName, verbosity),
Run: func(cmd *cobra.Command, args []string) {
// if err := openevec.ConfigCheck(*configName); err != nil {
// log.Fatalf("Config check failed %s", err)
// }
if err := openEVEC.SetupEden(*configName, configDir, softSerial, zedControlURL, ipxeOverride, grubOptions, netboot, installer); err != nil {

log.Fatalf("Setup eden failed: %s", err)
Expand Down
199 changes: 199 additions & 0 deletions docs/NEO-EDEN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# NeoEden: framework that allows you to write EVE tests in Golang

## Motivation

Personally, I don't believe in the docs, which are against "all bad" and for all "the good", especially in engineering practicies, because definition of good and bad depends on the context of the problem: things evolve, requirements change and what used to be perfect fit back then seems like really bad idea now. So how one should navigate uncertainty, choose methods and evolve products?

*(Pro Tip: As a software engineer, remember that the correct answer to 90% of questions is, of course, 'it depends'—because who doesn't love a good mix of nuance, edge cases, and never-ending trade-offs? And never trust random estimates you see on the Internet)*

I think there's no one right answer for that, and if you ask different people you would get different opinions. If you ask me, after some unecessary references and jokes it would come down to following

> “Programs must be written for people to read, and only incidentally for machines to execute.” ― Harold Abelson, Structure and Interpretation of Computer Programs

Check failure on line 11 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

blanks:end of line
So best practices would be

1) Define interface first, implement later (most probably first version would be not as good as you would like it to be)
2) Write documentation for people to understand contexts and ask less why? question when reading code or using it
3) Learn by doing, there's a lot of unknown unknowns. So ship code, get feedback and iterate on it.

With that in mind let's talk about tests in Eden till version 0.9.12

## What is wrong with old way of doing things? Why do we need new one?

Why do we have a section about how things use to be? Well, history have a tendency to repeat itself, people involved in the project change and there could be a newcomer, who would think that Domain Specific Language (DSL) could be a good way to go. It would be nice to have some kind of summary, which basically says, yep, we tried that, that's how we did it and those are the problems we faced, that is why we switched. It doesn't mean to discourage, but rather learn from past experiences, who knows, maybe in some time there will be addition saying that DSL actually is the best way to go for that time requirements. Change is the only constant :)

When talking about Eden tests till version 0.9.12, we are talking about escript, a Domain Specific Language (DSL) which describes test case and uses Eden to setup, environment. Escript looks like this

```bash
# 1 Setup environment variables
{{$port := "2223"}}
{{$network_name := "n1"}}
{{$app_name := "eclient"}}

# ...

# 2 run eden commands
eden -t 1m network create 10.11.12.0/24 -n {{$network_name}}

# 3 run escript commands
test eden.network.test -test.v -timewait 10m ACTIVATED {{$network_name}}

eden pod deploy -n {{$app_name}} --memory=512MB {{template "eclient_image"}} -p {{$port}}:22 --networks={{$network_name}}

# 4 execute shell script which are defined inside escript file
exec -t 5m bash ssh.sh
stdout 'Ubuntu'

# 5 overwrite configuration
-- eden-config.yml --
{{/* Test's config. file */}}
test:
controller: adam://{{EdenConfig "adam.ip"}}:{{EdenConfig "adam.port"}}
eve:
{{EdenConfig "eve.name"}}:
onboard-cert: {{EdenConfigPath "eve.cert"}}
serial: "{{EdenConfig "eve.serial"}}"
model: {{EdenConfig "eve.devmodel"}}
-- ssh.sh --
EDEN={{EdenConfig "eden.root"}}/{{EdenConfig "eden.bin-dist"}}/{{EdenConfig "eden.eden-bin"}}
for i in `seq 20`
do
sleep 20
# Test SSH-access to container
echo $i\) $EDEN sdn fwd eth0 {{$port}} -- {{template "ssh"}} grep Ubuntu /etc/issue
$EDEN sdn fwd eth0 {{$port}} -- {{template "ssh"}} grep Ubuntu /etc/issue && break
done
```
So you can
1) setup some environment variables
2) run eden commands
3) run escript commands with test
4) execute user-defined shell scripts
5) overwrite eden configuration
Escript file is fed as input to eden test command, which parses the variables using golang templates to substitute some variables using templates in golang, then it's going to read line by line and execute it via os.exec call in golang. test is actually a compiled golang binary, we compile it inside eden repo and then execute it, under the hood it is a manually forked version of standard golang test package with some added commands.

Sounds a tad complicated, doesn't it?
So there are two problems with that approach:
**You have to be an escript expert**: when new person comes to a project, they will have some industry skills, like C++ expertise, Rust, Golang, Computer Networking, etc. but if you never worked on this project, you never heard about escript, its' sole purpose was to be part of Eden and be useful language to describe tests for Eden. One might argue, but that's just bash on steroids. Well, true, but do you know what `!` means? It's not equal parameter in escript. So it is like bash, but not exactly it, and it might take time to figure out other hidden features, that could be solved by proper documentation. But before spiraling down that conversation lets think about tools? Imagine, that you wrote new fancy eden test and it doesn't work. Usual thing, you would say. Test Driven Development is all about writing failing tests first and then making them work.
**But how do you debug that test?** Because you're running an interpreter, which runs bash commands via os.exec plus you execute golang program, which is written and compiled inside the repository. Debug printing would work, but I find debuggers much more useful and efficient most of the times. It is a matter of taste, but better to debugger at your disposal, than not to have it in this case. Even worse, how do you debug escript problem? You need to know it's internals, there's no Stackoverflow or Google by your side, nor there are books about it.

**Bumping golang is actually manual action**: last but not least, we have to maintain custom test files which a basically copy-paste with one added function, bumping golang turns into some weird dances in that case.

I believe that those reasons are good enough to switch for golang-only approach to write tests, which is called NeoEden (kudos to @shjala for the name)

Also worst case what could happen with DSL is something like the Inner JSON effect [link](https://thedailywtf.com/articles/the-inner-json-effect)

## Enter NeoEden. How tests look now?

They look like [this](../tests/sec/sec_test.go) code listing is in the end of this section. Conceptually, Eden contains of two entities: an entity which allows you setup some kind of environment and deploy EVE there. Think Infrastructure as a Code, like Teraform, which allows me using golang code to deploy, this is openevec package; second entity is evetestkit, which serves as an abstraction over missing features of adam as controller such as keeping track of state and provide you with toolkit to test things, namely send commands and access EVE or its AppInstances to see if the certain file exists or certain request gives you certain response. Those two things let you create golang-only test like one below. You use openevec to setup environment and evetestkit to help you describe what is passing criterion for particular test. The common denominator between openevec and evetestkit is EdenSetupArgs, openevec is just a wrapper over that configuration, which defines your state. The goal is to be able to define your state, write custom functions to state, like this:


```go
pe := openevec.GetDefaultPatchEnvelope()
appInstance := openevec.GetDefaultAppInstance()
cfg := openevec.GetDefaultConfig(path)
.WithHyperVisor(openevec::HyperVisorQemu)
.WithAppInstances(appInstance)
.WithPatchEnvelopes(pe)
evec := openvec.CreateOpnEVEC(cfg)
evec.SetupEden(/* ... */)
evec.StartEden(/* ... */)
evec.OnboardEve(/* ... */)
node, err := tk.InitializeTestFromConfig(cfg)
got := node.GetPatchEnvelopes(appInstance.UUID)
t.assert_eq(pe, got)
got = node.AppInstanceSSH(fmt.Sprintf("curl %s", openevec.DEFAULT_PATH_ENVELOPE_URL))
t.assert_eq(pe, got)
```

This is an example of how test for patch envelopes should look like. You might not know what PatchEnvelopes are, but from the definition you can see what does it mean to get patch envelopes and that if you ssh to AppInstance and do curl to some URL you will get that patch envelope. Below is an working example of a test which checks if app armor is enabled (if there is a file on a file system on EVE)

```go
package sec_test
import (
"os"

Check failure on line 130 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
"path/filepath"

Check failure on line 131 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
"strings"

Check failure on line 132 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
"testing"

Check failure on line 133 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
"github.com/lf-edge/eden/pkg/defaults"

Check failure on line 135 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
tk "github.com/lf-edge/eden/pkg/evetestkit"

Check failure on line 136 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
"github.com/lf-edge/eden/pkg/openevec"

Check failure on line 137 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
log "github.com/sirupsen/logrus"

Check failure on line 138 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
)
const projectName = "security-test"
const appArmorStatus = "/sys/module/apparmor/parameters/enabled"
var eveNode *tk.EveNode
func TestMain(m *testing.M) {
log.Println("Security Test Suite started")

Check failure on line 147 in docs/NEO-EDEN.md

View workflow job for this annotation

GitHub Actions / yetus

markdownlint:MD010/no-hard-tabs Hard tabs [Column: 1]
defer log.Println("Security Test Suite finished")
currentPath, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
twoLevelsUp := filepath.Dir(filepath.Dir(currentPath))
cfg := openevec.GetDefaultConfig(twoLevelsUp)
if err = openevec.ConfigAdd(cfg, cfg.ConfigName, "", false); err != nil {
log.Fatal(err)
}
evec := openevec.CreateOpenEVEC(cfg)
configDir := filepath.Join(twoLevelsUp, "eve-config-dir")
if err := evec.SetupEden("config", configDir, "", "", "", []string{}, false, false); err != nil {
log.Fatalf("Failed to setup Eden: %v", err)
}
if err := evec.StartEden(defaults.DefaultVBoxVMName, "", ""); err != nil {
log.Fatalf("Start eden failed: %s", err)
}
if err := evec.OnboardEve(cfg.Eve.CertsUUID); err != nil {
log.Fatalf("Eve onboard failed: %s", err)
}
node, err := tk.InitilizeTestFromConfig(projectName, cfg, tk.WithControllerVerbosity("debug"))
if err != nil {
log.Fatalf("Failed to initialize test: %v", err)
}
eveNode = node
res := m.Run()
os.Exit(res)
}
func TestAppArmorEnabled(t *testing.T) {
log.Println("TestAppArmorEnabled started")
defer log.Println("TestAppArmorEnabled finished")
t.Parallel()
out, err := eveNode.EveReadFile(appArmorStatus)
if err != nil {
t.Fatal(err)
}
exits := strings.TrimSpace(string(out))
if exits != "Y" {
t.Fatal("AppArmor is not enabled")
}
}
```
44 changes: 0 additions & 44 deletions pkg/openevec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"io"
"os"
"path"
"path/filepath"
"reflect"
"strings"

Expand Down Expand Up @@ -325,49 +324,6 @@ func resolvePath(path string, v reflect.Value) {
}
}

func ConfigCheck(configName string) error {
configFile := utils.GetConfig(configName)
configSaved := utils.ResolveAbsPath(fmt.Sprintf("%s-%s", configName, defaults.DefaultConfigSaved))

abs, err := filepath.Abs(configSaved)
if err != nil {
return fmt.Errorf("fail in reading filepath: %s\n", err.Error())
}

if _, err = os.Lstat(abs); os.IsNotExist(err) {
if err = utils.CopyFile(configFile, abs); err != nil {
return fmt.Errorf("copying fail %s\n", err.Error())
}
} else {

viperLoaded, err := utils.LoadConfigFile(abs)
if err != nil {
return fmt.Errorf("error reading config %s: %s\n", abs, err.Error())
}
if viperLoaded {
confOld := viper.AllSettings()

if _, err = utils.LoadConfigFile(configFile); err != nil {
return fmt.Errorf("error reading config %s: %s", configFile, err.Error())
}

confCur := viper.AllSettings()

if reflect.DeepEqual(confOld, confCur) {
log.Infof("Config file %s is the same as %s\n", configFile, configSaved)
} else {
return fmt.Errorf("the current configuration file %s is different from the saved %s. You can fix this with the commands 'eden config clean' and 'eden config add/set/edit'.\n", configFile, abs)
}
} else {
/* Incorrect saved config -- just rewrite by current */
if err = utils.CopyFile(configFile, abs); err != nil {
return fmt.Errorf("copying fail %s\n", err.Error())
}
}
}
return nil
}

func getValStrRepr(v reflect.Value) string {
if v.Kind() == reflect.String {
return fmt.Sprintf("'%v'", v.Interface())
Expand Down
24 changes: 12 additions & 12 deletions pkg/openevec/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
uuid "github.com/satori/go.uuid"
)

func GetDefaultConfig(currentPath string) *EdenSetupArgs {
func GetDefaultConfig(projectRootPath string) *EdenSetupArgs {
ip, err := utils.GetIPForDockerAccess()
if err != nil {
return nil
Expand All @@ -29,8 +29,8 @@ func GetDefaultConfig(currentPath string) *EdenSetupArgs {
return nil
}

imageDist := filepath.Join(currentPath, defaults.DefaultDist, fmt.Sprintf("%s-%s", defaults.DefaultContext, defaults.DefaultImageDist))
certsDist := filepath.Join(currentPath, defaults.DefaultDist, fmt.Sprintf("%s-%s", defaults.DefaultContext, defaults.DefaultCertsDist))
imageDist := filepath.Join(projectRootPath, defaults.DefaultDist, fmt.Sprintf("%s-%s", defaults.DefaultContext, defaults.DefaultImageDist))
certsDist := filepath.Join(projectRootPath, defaults.DefaultDist, fmt.Sprintf("%s-%s", defaults.DefaultContext, defaults.DefaultCertsDist))

firmware := []string{filepath.Join(imageDist, "eve", "OVMF.fd")}
if runtime.GOARCH == "amd64" {
Expand All @@ -41,11 +41,11 @@ func GetDefaultConfig(currentPath string) *EdenSetupArgs {

defaultEdenConfig := &EdenSetupArgs{
Eden: EdenConfig{
Root: filepath.Join(currentPath, defaults.DefaultDist),
Tests: filepath.Join(currentPath, defaults.DefaultDist, "tests"),
Root: filepath.Join(projectRootPath, defaults.DefaultDist),
Tests: filepath.Join(projectRootPath, defaults.DefaultDist, "tests"),
Download: true,
BinDir: defaults.DefaultBinDist,
SSHKey: filepath.Join(currentPath, defaults.DefaultDist, fmt.Sprintf("%s-%s", defaults.DefaultContext, defaults.DefaultSSHKey)),
SSHKey: filepath.Join(projectRootPath, defaults.DefaultDist, fmt.Sprintf("%s-%s", defaults.DefaultContext, defaults.DefaultSSHKey)),
CertsDir: certsDist,
TestBin: defaults.DefaultTestProg,
EdenBin: "eden",
Expand Down Expand Up @@ -134,8 +134,8 @@ func GetDefaultConfig(currentPath string) *EdenSetupArgs {
QemuMemory: defaults.DefaultMemory,
ImageSizeMB: defaults.DefaultEVEImageSize,
Serial: defaults.DefaultEVESerial,
Pid: filepath.Join(currentPath, defaults.DefaultDist, fmt.Sprintf("%s-eve.pid", strings.ToLower(defaults.DefaultContext))),
Log: filepath.Join(currentPath, defaults.DefaultDist, fmt.Sprintf("%s-eve.log", strings.ToLower(defaults.DefaultContext))),
Pid: filepath.Join(projectRootPath, defaults.DefaultDist, fmt.Sprintf("%s-eve.pid", strings.ToLower(defaults.DefaultContext))),
Log: filepath.Join(projectRootPath, defaults.DefaultDist, fmt.Sprintf("%s-eve.log", strings.ToLower(defaults.DefaultContext))),
TelnetPort: defaults.DefaultTelnetPort,
TPM: defaults.DefaultTPMEnabled,
ImageFile: filepath.Join(imageDist, "eve", "live.img"),
Expand Down Expand Up @@ -178,16 +178,16 @@ func GetDefaultConfig(currentPath string) *EdenSetupArgs {
Sdn: SdnConfig{
RAM: defaults.DefaultSdnMemory,
CPU: defaults.DefaultSdnCpus,
ConsoleLogFile: filepath.Join(currentPath, defaults.DefaultDist, "sdn-console.log"),
ConsoleLogFile: filepath.Join(projectRootPath, defaults.DefaultDist, "sdn-console.log"),
Disable: true,
TelnetPort: defaults.DefaultSdnTelnetPort,
MgmtPort: defaults.DefaultSdnMgmtPort,
PidFile: filepath.Join(currentPath, defaults.DefaultDist, "sdn.pid"),
PidFile: filepath.Join(projectRootPath, defaults.DefaultDist, "sdn.pid"),
SSHPort: defaults.DefaultSdnSSHPort,
SourceDir: filepath.Join(currentPath, "sdn"),
SourceDir: filepath.Join(projectRootPath, "sdn"),
ConfigDir: filepath.Join(edenDir, fmt.Sprintf("%s-sdn", "default")),
ImageFile: filepath.Join(imageDist, "eden", "sdn-efi.qcow2"),
LinuxkitBin: filepath.Join(currentPath, defaults.DefaultBuildtoolsDir, "linuxkit"),
LinuxkitBin: filepath.Join(projectRootPath, defaults.DefaultBuildtoolsDir, "linuxkit"),
NetModelFile: "",
},

Expand Down
1 change: 0 additions & 1 deletion pkg/openevec/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ func InitVarsFromConfig(cfg *EdenSetupArgs) (*utils.ConfigVars, error) {
}

func Test(tstCfg *TestArgs) error {
fmt.Println("SOME TEST")
switch {
case tstCfg.TestList != "":
tests.RunTest(tstCfg.TestProg, []string{"-test.list", tstCfg.TestList}, "", tstCfg.TestTimeout, tstCfg.FailScenario, tstCfg.ConfigFile, tstCfg.Verbosity)
Expand Down
48 changes: 0 additions & 48 deletions shell-scripts/activate.csh.tmpl

This file was deleted.

Loading

0 comments on commit 6becead

Please sign in to comment.