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
56 changes: 56 additions & 0 deletions cmd/unikraft/integration/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,62 @@ cmd: ["cat", "/sys/class/uio/uio0/device/startdata"]
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 := uniq(), uniq(), uniq()

// A relay points at an interface, and the generated interface name is
// not predictable - it falls back to eth-<suffix> whenever
// network_interfaces is given explicitly - so the router needs one
// named up front to aim at.
Comment on lines +241 to +244

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.

No need to explain this I think?

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)

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})

// 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) {
r := runner(t, true, []string{staging, stable})
instName := uniq()
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.

82 changes: 77 additions & 5 deletions internal/cmd/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ type InstanceCreateCmd struct {
Volume []InstanceVolume `group:"flag-create" shortcut:"volumes" short:"v" sep:"none" help:"Attach volume." placeholder:"<name>:<path>[:<options>]" example:"my-vol:/data,cache:/tmp:ro,data:/mnt:size=10GiB"`
Rom []InstanceRom `group:"flag-create" shortcut:"roms" sep:"none" help:"Attach ROM." placeholder:"image=<ref>,at=<path>" example:"image=myuser/my-rom:latest\\,at=/rom0\\,name=my-rom,dir=./mydata\\,at=/rom"`

Network []InstanceNetwork `group:"flag-create" shortcut:"networks" 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"`
Comment thread
jedevc marked this conversation as resolved.

Service InstanceService `group:"flag-create" shortcut:"service" help:"Service group name or key." placeholder:"name"`
Publish []Service `group:"flag-create" shortcut:"service.services" short:"p" sep:"none" help:"Publish port." placeholder:"<src>:<dest>[/<handlers>]" example:"443:8080/http+tls"`
Domain []Domain `group:"flag-create" shortcut:"service.domains" sep:"none" help:"Service domain." placeholder:"fqdn" example:"example.com"`
Expand Down Expand Up @@ -194,8 +196,8 @@ type Instance struct {
Volumes []*InstanceVolume `mirror:"instance.volumes" field:",embed" create:"set" edit:"add,del=strings"`
Roms []*InstanceRom `mirror:"instance.roms" field:",embed" create:"set" edit:"set,add,del=strings"`

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"`
Gpus []InstanceGpu `mirror:"instance.gpus" field:"gpus,embed"`

Timestamps struct {
Created types.RelativeTime `mirror:"instance.created_at" field:",short"`
Expand Down Expand Up @@ -244,9 +246,50 @@ 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:"uuid,omitempty" field:",long"`
PrivateIP string `name:"-" mirror:"private_ip" json:"private-ip,omitempty" 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"`
DNS *bool `name:"dns" mirror:"relay_dns" json:"dns,omitempty" field:",long"`

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.

I assume the mirror here is correct? 🤔

}

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 @@ -1162,6 +1205,35 @@ func (Instance) Create(ctx context.Context, fields []resource.Field) ([]resource
}
req.Roms = append(req.Roms, reqRom)
}
case "networks":
for _, net := range field.Create.Set.([]*InstanceNetwork) {
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)
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