Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions cmd/unikraft/integration/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,66 @@ func TestInstances(t *testing.T) {
r.Run(t, []string{"unikraft", "instance", "delete", "test-" + instName})
})

t.Run("create-relay", func(t *testing.T) {
r := runner(t, true, []string{staging, stable})
routerName, clientName, optOutName, byUUIDName := uniq(), uniq(), uniq(), uniq()

iface := "test-" + routerName + "-eth0"
create := func(name string, opts ...string) []string {
return append([]string{
"unikraft", "instance", "create",
"--name", "test-" + name,
"--metro", r.Config.MetroName,
"--image", "nginx:latest",
"--memory", "128",
"--vcpus", "1",
"--set", "autostart=false",
}, opts...)
}

r.Run(t, create(routerName, "--network", "name="+iface, "--output", "quiet"))

out := r.Run(t, create(clientName, "--network", "relay.name="+iface))
assert.Regexp(t, `relay:`, out)
assert.Regexp(t, `name:\s+`+regexp.QuoteMeta(iface), out)
assert.Regexp(t, `dns:\s+true`, out)

// relay.dns is a dotted key rather than a nested value because relay=
// would take the whole value as the interface name.
out = r.Run(t, create(optOutName, "--network", "relay.name="+iface+",relay.dns=false"))
assert.Regexp(t, `dns:\s+false`, out)

ifaceUUID := strings.TrimSpace(r.Run(t, []string{
"unikraft", "instance", "get", "test-" + routerName,
"--output", "template={{ (index .networks 0).uuid }}",
}))
require.NotEmpty(t, ifaceUUID)
out = r.Run(t, create(byUUIDName, "--network", "relay.uuid="+ifaceUUID))
assert.Regexp(t, `uuid:\s+`+regexp.QuoteMeta(ifaceUUID), out)

out = r.Run(t, create(uniq(), "--network", "relay.name=test-"+routerName+"-nonexistent"), integ.ExpectFail())
assert.Regexp(t, `Invalid relay`, out)

// Relay chains are rejected, so the client's own interface cannot
// itself be relayed through.
clientIface := strings.TrimSpace(r.Run(t, []string{
"unikraft", "instance", "get", "test-" + clientName,
"--output", "template={{ (index .networks 0).name }}",
}))
require.NotEmpty(t, clientIface)
out = r.Run(t, create(uniq(), "--network", "relay.name="+clientIface), integ.ExpectFail())
assert.Regexp(t, `Invalid relay`, out)

r.Run(t, []string{"unikraft", "instance", "delete", "test-" + clientName, "test-" + optOutName, "test-" + byUUIDName})

// The relay's datapath is torn down asynchronously after its last
// client goes, and until it is the target instance deletes as -EBUSY.
require.Eventually(t, func() bool {
_, err := r.RunRaw(t, []string{"unikraft", "instance", "delete", "test-" + routerName}, integ.WithoutCancel())
return err == nil
}, 2*time.Minute, 5*time.Second, "relay target never became deletable")
})

t.Run("create-oom", func(t *testing.T) {
// TODO: Add 'stable' back when it runs platform version 13. Older
// versions send a duplicate "status" member that breaks every wait.
Expand Down
78 changes: 69 additions & 9 deletions cmd/unikraft/testdata/TestHelp/instances

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion cmd/unikraft/testdata/TestHelp/run

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 76 additions & 5 deletions internal/cmd/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ type Instance struct {
Roms []*InstanceRom `mirror:"instance.roms" field:",embed" create:"set" edit:"set,add,del=strings" flag:"rom" sep:"none" help:"Attach ROM." placeholder:"name=<name>,image=<ref>,at=<path>" example:"name=my-rom\\,image=myuser/my-rom:latest\\,at=/rom0,name=mydata\\,dir=./mydata\\,at=/rom"`
Plugins []*InstancePlugin `mirror:"instance.plugins" field:",embed" create:"set" edit:"set,add,del=strings" flag:"plugin" sep:"none" help:"Load plugin into the instance." placeholder:"name=<name>,image=<ref>[,config=<json>]" example:"name=sandbox\\,image=plugins/sandbox:latest,name=sandbox\\,image=plugins/sandbox:latest\\,config={\"persist_path\":\"/data\"}"`

Networks []InstanceNetwork `mirror:"instance.network_interfaces" field:",embed"`
Gpus []InstanceGpu `mirror:"instance.gpus" field:"gpus,embed"`
Networks []*InstanceNetwork `mirror:"instance.network_interfaces" field:",embed" create:"set" flag:"network" sep:"none" help:"Attach network interface.\n name: interface name\n relay.name: interface to route all traffic through\n relay.uuid: same, by uuid\n relay.dns: whether the relay forwards DNS (default true)\n ip: address in CIDR notation, requires tap-name\n mac: address, requires tap-name\n tap-name: TAP device to bring your own interface\n autoconfig: whether the guest configures the interface itself" placeholder:"<key>=<value>" example:"relay.name=my-router-eth0,relay.name=my-router-eth0\\,relay.dns=false,name=eth1\\,tap-name=tap0\\,ip=10.0.0.5/24"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no better way to represent sub-structs? this is ugly 😔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My weigh is in

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, is this okay then? Or do you have better suggestions 😓

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok for now, as I have no better suggestion 😔

Gpus []InstanceGpu `mirror:"instance.gpus" field:"gpus,embed"`

Timestamps struct {
Created types.RelativeTime `mirror:"instance.created_at" field:",short"`
Expand Down Expand Up @@ -178,9 +178,51 @@ type Instance struct {
}

type InstanceNetwork struct {
UUID string `mirror:"uuid" field:",long"`
PrivateIP string `mirror:"private_ip" field:",long"`
MAC string `mirror:"mac" field:",long"`
Name string `name:"name" mirror:"name" json:"name,omitempty" field:",long"`
UUID string `name:"-" mirror:"uuid" json:"-" field:",long"`
PrivateIP string `name:"-" mirror:"private_ip" json:"-" field:",long"`
MAC string `name:"mac" mirror:"mac" json:"mac,omitempty" field:",long"`
TapName string `name:"tap-name" mirror:"tap_name" json:"tap-name,omitempty" field:"tap-name,long"`

Relay *InstanceNetworkRelay `name:"relay" mirror:"relay" json:"relay,omitempty" field:",embed"`

IP string `name:"ip" json:"ip,omitempty" field:"ip,invisible"`
Autoconfig *bool `name:"autoconfig" mirror:"autoconfig" json:"autoconfig,omitempty" field:",long"`
}

// InstanceNetworkRelay is the interface all of this interface's traffic is
// routed through. The target is another instance's interface, which has no
// API of its own, so this holds plain identifiers rather than a Link.
type InstanceNetworkRelay struct {
Name string `name:"name" mirror:"name" json:"name,omitempty" field:",long"`
UUID string `name:"uuid" mirror:"uuid" json:"uuid,omitempty" field:",long"`
// The API names this member relay_dns, inside the relay object itself.
DNS *bool `name:"dns" mirror:"relay_dns" json:"dns,omitempty" field:",long"`
}

func (n *InstanceNetwork) UnmarshalText(data []byte) error {
type alias InstanceNetwork
parsed, err := value.Parse[alias]([]string{string(data)})
if err != nil {
return err
}
*n = InstanceNetwork(parsed)
if n.Relay != nil && n.Relay.Name == "" && n.Relay.UUID == "" {
return fmt.Errorf("relay requires relay.name or relay.uuid")
}
return nil
}

func (n *InstanceNetwork) UnmarshalJSON(data []byte) error {
if len(data) != 0 && data[0] == '"' {
var text string
if err := json.Unmarshal(data, &text); err != nil {
return err
}
return n.UnmarshalText([]byte(text))
}
type networkJSON InstanceNetwork // alias to avoid recursion
return json.Unmarshal(data, (*networkJSON)(n))
}

type InstanceGpu struct {
Expand Down Expand Up @@ -1349,6 +1391,35 @@ func (Instance) Create(ctx context.Context, fields []resource.Field) ([]resource
}
req.Plugins = append(req.Plugins, reqPlugin)
}
case "networks":
for _, net := range field.Create.Set.([]*InstanceNetwork) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - uuid and private-ip are now json:"-" as well as name:"-", so neither spelling advertises them. The compact form still errors on uuid=; the JSON form now drops it as an unknown member, which is what it does for every other unknown member on every other type here. Covered by a new TestCreatePatches case.

reqNet := platform.CreateInstanceRequestNetworkInterface{
Name: ptr.NilIfZero(net.Name),
TapName: ptr.NilIfZero(net.TapName),
Ip: ptr.NilIfZero(net.IP),
Autoconfig: net.Autoconfig,
}
if net.Relay != nil {
if net.Relay.Name == "" && net.Relay.UUID == "" {
return nil, fmt.Errorf("relay requires a name or uuid")
}
reqNet.Relay = &platform.NetworkInterfaceRelay{
Name: ptr.NilIfZero(net.Relay.Name),
Uuid: ptr.NilIfZero(net.Relay.UUID),
RelayDns: net.Relay.DNS,
}
}
if net.MAC != "" {
// HACK: the spec's network interface omits mac, though
// /v1/instances has accepted it since MAC-only custom
// interfaces landed.
macJSON, _ := json.Marshal(net.MAC)
Comment on lines +1413 to +1416

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, why is the mac field not in the spec? 🤔

reqNet.AdditionalProperties = map[string]jsontext.Value{
"mac": jsontext.Value(macJSON),
}
}
req.NetworkInterfaces = append(req.NetworkInterfaces, reqNet)
}
case "service":
svc := field.Create.Set.(*InstanceService)
if req.ServiceGroup == nil {
Expand Down
Loading
Loading