Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ form.
hash of the current file, and `ps` marks changed instances (`config_changed`
in JSON). (by @tallica)

- Support for declarative Network ACLs (firewall rules between compose networks)
via `networks.<name>.x-incus-compose.acl` (`default`, `ingress`, and `egress`
rules). Supports OVN-to-OVN network peer resolution (`@<network>/<peer>`) as
well as bridge subnet CIDR resolution across same-project and cross-project
(`<project>:<network>`) network references. (by @jochumdev, #98)

- Disk and tmpfs mount targets that are symlinks in the container rootfs (common
in `nix-containers` images such as `/etc/caddy` pointing into `/nix/store`)
are now materialized as real directories or files before starting the
Expand Down Expand Up @@ -131,6 +137,10 @@ form.

### Fixed

- `logs [SERVICE...]` and foreground `up [SERVICE...]` now filter log output to
the in-scope services (including scaled replicas) and reject unknown service
names. (by @jochumdev, #231)

- On a server without OVN, `up` and the `healthd`/`dns` commands no longer spend
about two seconds detecting that, several times per command, and OVN networks
in projects other than `default` are now found. (by @tallica)
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/ic-dns/ecs_view/go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module github.com/lxc/incus-compose/benchmarks/ic-dns/ecs_view

go 1.27.2
go 1.27.3

require (
github.com/coredns/coredns v1.14.7
github.com/coredns/coredns v1.15.0
github.com/hashicorp/go-immutable-radix/v2 v2.1.0
github.com/lxc/incus-compose v0.0.0
github.com/miekg/dns v1.1.73
Expand Down
24 changes: 10 additions & 14 deletions benchmarks/ic-dns/ecs_view/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coredns/caddy v1.1.4 h1:+Lls5xASB0QsA2jpCroCOwpPlb5GjIGlxdjXxdX0XVo=
github.com/coredns/caddy v1.1.4/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
github.com/coredns/coredns v1.14.7 h1:UPDkn4QN+xyNfjz3U5ldgoLszDYpMZEJoy3+ze9au4Q=
github.com/coredns/coredns v1.14.7/go.mod h1:ABNpFbWAas3/CDYRyzlVYLqX/gSVq/5yDr7wm5fjafA=
github.com/coredns/coredns v1.15.0 h1:OBT+OEyHVdUiVxPhrNlC5XZVq3RkSxzQXfZQ76gm5ck=
github.com/coredns/coredns v1.15.0/go.mod h1:WdP1PeJqL6WsOiIeC91TsdfjE2UXlpapsr/phk3IaH0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
Expand All @@ -31,13 +31,11 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0=
github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
github.com/prometheus/common v0.72.1 h1:ZAiQed/nk8IIcsdzb2TlJu23NEUDyDLQ9TXTN2WRK6Y=
github.com/prometheus/common v0.72.1/go.mod h1:1IoBY4IRaJnhs2ESjNzjilP1cJ/P8pimGoomq7zRXrA=
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
Expand All @@ -51,15 +49,13 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c=
golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
2 changes: 1 addition & 1 deletion benchmarks/ic-dns/kubernetes/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/lxc/incus-compose/benchmarks/ic-dns/kubernetes

go 1.27.2
go 1.27.3

// Pinned to what CoreDNS itself requires, taken from the checkout in
// work/vendor/coredns. The kubernetes plugin imports packages that later
Expand Down
24 changes: 11 additions & 13 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,45 +107,43 @@ func (c *GlobalClient) newProjectClient(name, incusName string, created bool, pr
if c.IsDebugging() {
cp.logger = cp.logger.With("incus_project", incusName)
cp.LogDebug("Running on network type", "type", cp.NetworkType())
}

c.projectsMu.Lock()
c.projects = append(c.projects, cp)
c.projectsMu.Unlock()

if c.IsDebugging() {
// Debug logging hooks
c.AddHookBefore(func(_ context.Context, action Action, r Resource, args Options, err error) error {
cp.AddHookBefore(func(_ context.Context, action Action, r Resource, args Options, err error) error {
if r.Kind() == KindNetwork {
net, ok := r.(*Network)
if ok {
c.LogDebug("Running", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "network_type", net.Config.Type)
cp.LogDebug("Running", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "network_type", net.Config.Type)
return err
}
}

c.LogDebug("Running", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName())
cp.LogDebug("Running", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName())
return err
})
c.AddHookAfter(func(_ context.Context, action Action, r Resource, args Options, err error) error {
cp.AddHookAfter(func(_ context.Context, action Action, r Resource, args Options, err error) error {
if err != nil {
c.LogWarn("Result with error", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "created", r.Created(), "error", err)
cp.LogWarn("Result with error", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "created", r.Created(), "error", err)
return err
}

if r.Kind() == KindNetwork {
net, ok := r.(*Network)
if ok {
c.LogDebug("Run", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "created", r.Created(), "network_type", net.Config.Type)
cp.LogDebug("Run", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "created", r.Created(), "network_type", net.Config.Type)
return nil
}
}

c.LogDebug("Run", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "created", r.Created())
cp.LogDebug("Run", "action", action, "kind", r.Kind(), "name", r.Name(), "incus_name", r.IncusName(), "created", r.Created())
return nil
})
}

c.projectsMu.Lock()
c.projects = append(c.projects, cp)
c.projectsMu.Unlock()

return cp, nil
}

Expand Down
12 changes: 10 additions & 2 deletions client/global_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ type ClientConfig struct {

// ToolsMount is the mount path for helper tools.
ToolsMount string

// AllowRemoteMounts permits pass-through bind mounts on a remote Incus server.
AllowRemoteMounts bool
}

// ClientOption is a functional option for configuring the Client.
Expand Down Expand Up @@ -169,6 +172,11 @@ func ClientLocksVolume(n string) ClientOption {
return func(c *ClientConfig) { c.LocksVolume = n }
}

// ClientAllowRemoteMounts permits pass-through bind mounts on a remote Incus server.
func ClientAllowRemoteMounts(allow bool) ClientOption {
return func(c *ClientConfig) { c.AllowRemoteMounts = allow }
}

// GlobalClient provides a high-level interface to Incus operations.
type GlobalClient struct {
ctx context.Context
Expand Down Expand Up @@ -379,8 +387,8 @@ func NewTestClient(ctx context.Context) (*GlobalClient, error) {

// NewOfflineClient creates a disconnected project client for resource planning.
// It can create in-memory resources, but cannot run Incus operations.
func NewOfflineClient(ctx context.Context, projectName string) *Client {
gc := New(ctx)
func NewOfflineClient(ctx context.Context, projectName string, opts ...ClientOption) *Client {
gc := New(ctx, opts...)
gc.unix = true
config := gc.config
config.DescriptionFormat = fmt.Sprintf(config.DescriptionFormat, projectName) + ":%s"
Expand Down
11 changes: 11 additions & 0 deletions client/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ func SanitizeIncusName(name string, maxLength int) string {

return safe
}

// SanitizePeerName converts a project and network name to a valid Incus
// network peer name (1-63 alphanumeric or hyphen characters).
func SanitizePeerName(projectName, networkName string) string {
full := networkName
if projectName != "" {
full = projectName + "-" + networkName
}

return SanitizeIncusName(full, MaxIncusNameLen)
}
115 changes: 101 additions & 14 deletions client/resource_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type NetworkConfig struct {
// ACL, when set, is created and attached to this network.
ACL *incusApi.NetworkACLsPost

// ACLNetworks maps network references in ACL rules to their target Network
// resources so Start can resolve them to peers or CIDRs after Ensure.
ACLNetworks map[string]*Network

// Peers are the peer relationships to create on this network.
Peers []incusApi.NetworkPeersPost
}
Expand Down Expand Up @@ -227,14 +231,7 @@ func (r *Network) Ensure(ctx context.Context, opts ...Option) error {
}

if err == nil {
if options.Create {
err = r.ensurePeers(ctx)
if err == nil {
err = r.ensureACLs(ctx)
}
}

err = r.client.hookAfter(ctx, ActionEnsure, r, options, err)
err = r.client.hookAfter(ctx, ActionEnsure, r, options, nil)

return err
}
Expand All @@ -259,16 +256,105 @@ func (r *Network) Ensure(ctx context.Context, opts ...Option) error {
err = nil
}

if err == nil && options.Create {
err = r.ensurePeers(ctx)
if err == nil {
err = r.ensureACLs(ctx)
err = r.client.hookAfter(ctx, ActionEnsure, r, options, err)

return err
}

// Start resolves ACL rules and configures peers and ACLs on the network.
func (r *Network) Start(ctx context.Context, opts ...Option) error {
r.mu.Lock()
defer r.mu.Unlock()

options := NewOptions(opts...)

err := r.client.hookBefore(ctx, ActionStart, r, options, nil)
if err != nil {
return err
}

if !r.IsEnsured() {
return r.client.hookAfter(ctx, ActionStart, r, options, ErrNotEnsured)
}

if r.Config.External {
return r.client.hookAfter(ctx, ActionStart, r, options, nil)
}

var resolved map[string]string
var extraPeers []incusApi.NetworkPeersPost

if r.Config.ACL != nil && len(r.Config.ACLNetworks) > 0 {
resolved = make(map[string]string, len(r.Config.ACLNetworks))
localOVN := r.Type(ctx) == "ovn"

for ref, target := range r.Config.ACLNetworks {
if !target.IsEnsured() {
ensureErr := target.Ensure(ctx, OptionCreate())
if ensureErr != nil {
err = fmt.Errorf("network %q: resolving ACL target %q: %w", r.Name(), ref, ensureErr)

return r.client.hookAfter(ctx, ActionStart, r, options, err)
}
}

if localOVN && target.Type(ctx) == "ovn" {
peerName := SanitizePeerName(target.incusProject(), target.IncusName())
peer := incusApi.NetworkPeersPost{
Name: peerName,
TargetProject: target.incusProject(),
TargetNetwork: target.IncusName(),
}

if !slices.ContainsFunc(r.Config.Peers, func(p incusApi.NetworkPeersPost) bool {
return p.TargetProject == peer.TargetProject && p.TargetNetwork == peer.TargetNetwork
}) && !slices.ContainsFunc(extraPeers, func(p incusApi.NetworkPeersPost) bool {
return p.TargetProject == peer.TargetProject && p.TargetNetwork == peer.TargetNetwork
}) {
extraPeers = append(extraPeers, peer)
}

recip := incusApi.NetworkPeersPost{
Name: SanitizePeerName(r.incusProject(), r.IncusName()),
TargetProject: r.incusProject(),
TargetNetwork: r.IncusName(),
}

recipErr := r.ensurePeerOn(ctx, target.incusProject(), target.IncusName(), recip)
if recipErr != nil {
err = fmt.Errorf("network %q: creating reciprocal peer on %s/%s: %w", r.Name(), target.incusProject(), target.IncusName(), recipErr)

return r.client.hookAfter(ctx, ActionStart, r, options, err)
}

resolved[ref] = "@" + r.IncusName() + "/" + peerName
} else {
subnets, subErr := target.Subnets(ctx)
if subErr != nil {
err = fmt.Errorf("network %q: reading subnets of %q: %w", r.Name(), ref, subErr)

return r.client.hookAfter(ctx, ActionStart, r, options, err)
}

if len(subnets) == 0 {
err = fmt.Errorf("network %q: target network %q has no IPv4 or IPv6 subnet", r.Name(), ref)

return r.client.hookAfter(ctx, ActionStart, r, options, err)
}

resolved[ref] = strings.Join(subnets, ",")
}
}
}

err = r.client.hookAfter(ctx, ActionEnsure, r, options, err)
err = r.ensurePeers(ctx, extraPeers...)
if err != nil {
return r.client.hookAfter(ctx, ActionStart, r, options, err)
}

return err
err = r.ensureACLs(ctx, resolved)

return r.client.hookAfter(ctx, ActionStart, r, options, err)
}

func (r *Network) incusProject() string {
Expand Down Expand Up @@ -681,6 +767,7 @@ func (r *Network) updateDNSAliases(ctx context.Context, ownedServices []string,
var (
_ Resource = (*Network)(nil)
_ EnsureAble = (*Network)(nil)
_ StartAble = (*Network)(nil)
_ DeleteAble = (*Network)(nil)
)

Expand Down
Loading