apparmor: Check if apparmor_parser is available
- fixes: https://github.com/moby/moby/issues/44900
- related to: https://github.com/moby/moby/pull/42276

https://github.com/moby/moby/pull/42276 replaced the function we used to check whether AppArmor is supported on the host from the one in `runc/libcontainer/apparmor` to `containerd/pkg/apparmor`.
In containerd v1.6.1 this function has dropped a check for the `apparmor_parser` binary: https://github.com/containerd/containerd/pull/5519

It's possible in some environments that the apparmor will be enabled but the tool to load the profile is not available which will cause the `ensureDefaultAppArmorProfile` to fail completely.

This patch checks the apparmor_parser is available, if it's not, then the function returns early, but still logs a warning to the daemon log.

**- What I did**

**- How I did it**

**- How to verify it**

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->


**- A picture of a cute animal (not mandatory but encouraged)**


daemon/apparmor_default.go
//go:build linux
// +build linux

package daemon // import "github.com/docker/docker/daemon"

import (
	"fmt"

	"github.com/containerd/containerd/pkg/apparmor"
	aaprofile "github.com/docker/docker/profiles/apparmor"
)

// Define constants for native driver
const (
	unconfinedAppArmorProfile = "unconfined"
	defaultAppArmorProfile    = "docker-default"
)

// DefaultApparmorProfile returns the name of the default apparmor profile
func DefaultApparmorProfile() string {
	if apparmor.HostSupports() {
		return defaultAppArmorProfile
	}
	return ""
}

func ensureDefaultAppArmorProfile() error {
	if apparmor.HostSupports() {
		loaded, err := aaprofile.IsLoaded(defaultAppArmorProfile)
		if err != nil {
			return fmt.Errorf("Could not check if %s AppArmor profile was loaded: %s", defaultAppArmorProfile, err)
		}

		// Nothing to do.
		if loaded {
			return nil
		}

		// Load the profile.
		if err := aaprofile.InstallDefault(defaultAppArmorProfile); err != nil {
			return fmt.Errorf("AppArmor enabled on system but the %s profile could not be loaded: %s", defaultAppArmorProfile, err)
		}
	}

	return nil
}

//go:build linux
// +build linux

package daemon // import "github.com/docker/docker/daemon"

import (
	"fmt"
	"os"
	"sync"

	"github.com/containerd/containerd/pkg/apparmor"
	aaprofile "github.com/docker/docker/profiles/apparmor"
	"github.com/sirupsen/logrus"
)

// Define constants for native driver
const (
	unconfinedAppArmorProfile = "unconfined"
	defaultAppArmorProfile    = "docker-default"
)

var (
	checkAppArmorOnce   sync.Once
	isAppArmorAvailable bool
)

// DefaultApparmorProfile returns the name of the default apparmor profile
func DefaultApparmorProfile() string {
	if apparmor.HostSupports() {
		return defaultAppArmorProfile
	}
	return ""
}

func ensureDefaultAppArmorProfile() error {
	checkAppArmorOnce.Do(func() {
		if apparmor.HostSupports() {
			// Restore the apparmor_parser check removed in containerd:
			// https://github.com/containerd/containerd/commit/1acca8bba36e99684ee3489ea4a42609194ca6b9
			// Fixes: https://github.com/moby/moby/issues/44900
			if _, err := os.Stat("/sbin/apparmor_parser"); err == nil {
				isAppArmorAvailable = true
			} else {
				logrus.Warn("AppArmor enabled on system but \"apparmor_parser\" binary is missing, so profile can't be loaded")
			}
		}
	})

	if isAppArmorAvailable {
		loaded, err := aaprofile.IsLoaded(defaultAppArmorProfile)
		if err != nil {
			return fmt.Errorf("Could not check if %s AppArmor profile was loaded: %s", defaultAppArmorProfile, err)
		}

		// Nothing to do.
		if loaded {
			return nil
		}

		// Load the profile.
		if err := aaprofile.InstallDefault(defaultAppArmorProfile); err != nil {
			return fmt.Errorf("AppArmor enabled on system but the %s profile could not be loaded: %s", defaultAppArmorProfile, err)
		}
	}

	return nil
}

Ah right, thanks for catching this 😄 
As for logging - I think it makes sense to do it inside the `sync.Once`.
Doing it every time will simply spam the daemon log. And it could be a good thing as it definitely attracts more attention than one log line somewhere in the beginning.
However in this case it seems that in most cases where the cause of not having the `apparmor_parser` is just the characteristic of the environment rather than user's error - and he can't do anything about it.