Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fleet) store package installation in a bbolt db #25506

Merged
merged 9 commits into from
May 13, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 67 additions & 11 deletions cmd/installer/subcommands/installer/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (

// Commands returns the installer subcommands.
func Commands(_ *command.GlobalParams) []*cobra.Command {
return []*cobra.Command{bootstrapCommand(), installCommand(), removeCommand(), installExperimentCommand(), removeExperimentCommand(), promoteExperimentCommand(), garbageCollectCommand(), purgeCommand()}
return []*cobra.Command{bootstrapCommand(), installCommand(), removeCommand(), installExperimentCommand(), removeExperimentCommand(), promoteExperimentCommand(), garbageCollectCommand(), purgeCommand(), isInstalledCommand()}
}

type cmd struct {
Expand Down Expand Up @@ -77,7 +77,7 @@ type installerCmd struct {
installer.Installer
}

func newInstallerCmd(operation string) *installerCmd {
func newInstallerCmd(operation string) (*installerCmd, error) {
cmd := newCmd(operation)
var opts []installer.Option
if cmd.registry != "" {
Expand All @@ -86,11 +86,14 @@ func newInstallerCmd(operation string) *installerCmd {
if cmd.registryAuth != "" {
opts = append(opts, installer.WithRegistryAuth(cmd.registryAuth))
}
i := installer.NewInstaller(opts...)
i, err := installer.NewInstaller(opts...)
if err != nil {
return nil, err
}
return &installerCmd{
Installer: i,
cmd: cmd,
}
}, nil
}

type bootstraperCmd struct {
Expand Down Expand Up @@ -179,8 +182,11 @@ func installCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
i := newInstallerCmd("install")
i, err := newInstallerCmd("install")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
i.span.SetTag("params.url", args[0])
return i.Install(i.ctx, args[0])
},
Expand All @@ -195,8 +201,11 @@ func removeCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
i := newInstallerCmd("remove")
i, err := newInstallerCmd("remove")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
i.span.SetTag("params.package", args[0])
return i.Remove(i.ctx, args[0])
},
Expand All @@ -211,8 +220,11 @@ func purgeCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) (err error) {
i := newInstallerCmd("purge")
i, err := newInstallerCmd("purge")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
i.Purge(i.ctx)
return nil
},
Expand All @@ -227,8 +239,11 @@ func installExperimentCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
i := newInstallerCmd("install_experiment")
i, err := newInstallerCmd("install_experiment")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
i.span.SetTag("params.url", args[0])
return i.InstallExperiment(i.ctx, args[0])
},
Expand All @@ -243,8 +258,11 @@ func removeExperimentCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
i := newInstallerCmd("remove_experiment")
i, err := newInstallerCmd("remove_experiment")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
i.span.SetTag("params.package", args[0])
return i.RemoveExperiment(i.ctx, args[0])
},
Expand All @@ -259,8 +277,11 @@ func promoteExperimentCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
i := newInstallerCmd("promote_experiment")
i, err := newInstallerCmd("promote_experiment")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
i.span.SetTag("params.package", args[0])
return i.PromoteExperiment(i.ctx, args[0])
},
Expand All @@ -275,10 +296,45 @@ func garbageCollectCommand() *cobra.Command {
GroupID: "installer",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) (err error) {
i := newInstallerCmd("garbage_collect")
i, err := newInstallerCmd("garbage_collect")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
return i.GarbageCollect(i.ctx)
},
}
return cmd
}

const (
// ReturnCodeIsInstalledFalse is the return code when a package is not installed
ReturnCodeIsInstalledFalse = 10
)

func isInstalledCommand() *cobra.Command {
Copy link
Contributor

Choose a reason for hiding this comment

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

this command should be added in the e2e tests (agent, auto_inject, java tracer)

cmd := &cobra.Command{
Use: "is-installed <package>",
Short: "Check if a package is installed",
GroupID: "installer",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
i, err := newInstallerCmd("is_installed")
defer func() { i.Stop(err) }()
if err != nil {
return err
}
installed, err := i.IsInstalled(i.ctx, args[0])
if err != nil {
return err
}
if !installed {
// Return a specific code to differentiate from other errors
// `return err` will lead to a return code of -1
os.Exit(ReturnCodeIsInstalledFalse)
}
return nil
},
}
return cmd
}
5 changes: 5 additions & 0 deletions pkg/fleet/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type testPackageManager struct {
mock.Mock
}

func (m *testPackageManager) IsInstalled(ctx context.Context, pkg string) (bool, error) {
args := m.Called(ctx, pkg)
return args.Bool(0), args.Error(1)
}

func (m *testPackageManager) State(pkg string) (repository.State, error) {
args := m.Called(pkg)
return args.Get(0).(repository.State), args.Error(1)
Expand Down
83 changes: 64 additions & 19 deletions pkg/fleet/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import (
"net/http"
"os"
"path/filepath"
"slices"
"sync"
"time"

"github.com/DataDog/datadog-agent/pkg/fleet/installer/repository"
"github.com/DataDog/datadog-agent/pkg/fleet/installer/service"
"github.com/DataDog/datadog-agent/pkg/fleet/internal/db"
"github.com/DataDog/datadog-agent/pkg/fleet/internal/oci"
"github.com/DataDog/datadog-agent/pkg/util/filesystem"
"github.com/DataDog/datadog-agent/pkg/util/log"
Expand Down Expand Up @@ -43,6 +46,7 @@ var (

// Installer is a package manager that installs and uninstalls packages.
type Installer interface {
IsInstalled(ctx context.Context, pkg string) (bool, error)
State(pkg string) (repository.State, error)
States() (map[string]repository.State, error)

Expand All @@ -61,6 +65,7 @@ type Installer interface {
type installerImpl struct {
m sync.Mutex

db *db.PackagesDB
downloader *oci.Downloader
repositories *repository.Repositories
configsDir string
Expand Down Expand Up @@ -98,18 +103,27 @@ func WithRegistry(registry string) Option {
}

// NewInstaller returns a new Package Manager.
func NewInstaller(opts ...Option) Installer {
func NewInstaller(opts ...Option) (Installer, error) {
o := newOptions()
for _, opt := range opts {
opt(o)
}
err := ensurePackageDirExists()
if err != nil {
return nil, fmt.Errorf("could not ensure packages directory exists: %w", err)
}
db, err := db.New(filepath.Join(PackagesPath, "packages.db"), db.WithTimeout(10*time.Second))
if err != nil {
return nil, fmt.Errorf("could not create packages db: %w", err)
}
return &installerImpl{
db: db,
downloader: oci.NewDownloader(http.DefaultClient, o.registry, o.registryAuth),
repositories: repository.NewRepositories(PackagesPath, LocksPack),
configsDir: defaultConfigsDir,
tmpDirPath: TmpDirPath,
packagesDir: PackagesPath,
}
}, nil
}

// State returns the state of a package.
Expand All @@ -122,14 +136,19 @@ func (i *installerImpl) States() (map[string]repository.State, error) {
return i.repositories.GetState()
}

// IsInstalled checks if a package is installed.
func (i *installerImpl) IsInstalled(_ context.Context, pkg string) (bool, error) {
packages, err := i.db.ListPackages()
if err != nil {
return false, fmt.Errorf("could not list packages: %w", err)
}
return slices.Contains(packages, pkg), nil
}

// Install installs or updates a package.
func (i *installerImpl) Install(ctx context.Context, url string) error {
i.m.Lock()
defer i.m.Unlock()
err := i.preSetupPackage(ctx, packageDatadogInstaller)
if err != nil {
return fmt.Errorf("could not pre-setup package: %w", err)
}
pkg, err := i.downloader.Download(ctx, url)
if err != nil {
return fmt.Errorf("could not download package: %w", err)
Expand All @@ -156,7 +175,15 @@ func (i *installerImpl) Install(ctx context.Context, url string) error {
if err != nil {
return fmt.Errorf("could not create repository: %w", err)
}
return i.setupPackage(ctx, pkg.Name)
err = i.setupPackage(ctx, pkg.Name)
if err != nil {
return fmt.Errorf("could not setup package: %w", err)
}
err = i.db.CreatePackage(pkg.Name)
if err != nil {
return fmt.Errorf("could not store package installation in db: %w", err)
}
return nil
}

// InstallExperiment installs an experiment on top of an existing package.
Expand Down Expand Up @@ -224,12 +251,23 @@ func (i *installerImpl) Purge(ctx context.Context) {
i.m.Lock()
defer i.m.Unlock()

// todo check if agent/injector are installed
packages, err := i.db.ListPackages()
if err != nil {
// if we can't list packages we'll only remove the installer
packages = nil
log.Warnf("could not list packages: %v", err)
}
for _, pkg := range packages {
if pkg == packageDatadogInstaller {
continue
}
i.removePackage(ctx, pkg)
}
i.removePackage(ctx, packageDatadogInstaller)

// remove all from disk
span, _ := tracer.StartSpanFromContext(ctx, "remove_all")
err := os.RemoveAll(PackagesPath)
err = os.RemoveAll(PackagesPath)
defer span.Finish(tracer.WithError(err))
if err != nil {
log.Warnf("could not remove path: %v", err)
Expand All @@ -241,7 +279,15 @@ func (i *installerImpl) Remove(ctx context.Context, pkg string) error {
i.m.Lock()
defer i.m.Unlock()
i.removePackage(ctx, pkg)
return i.repositories.Delete(ctx, pkg)
err := i.repositories.Delete(ctx, pkg)
if err != nil {
return fmt.Errorf("could not delete repository: %w", err)
}
err = i.db.DeletePackage(pkg)
if err != nil {
return fmt.Errorf("could not remove package installation in db: %w", err)
}
return nil
}

// GarbageCollect removes unused packages.
Expand Down Expand Up @@ -274,15 +320,6 @@ func (i *installerImpl) stopExperiment(ctx context.Context, pkg string) error {
}
}

func (i *installerImpl) preSetupPackage(_ context.Context, pkg string) error {
switch pkg {
case packageDatadogInstaller:
return service.PreSetupInstaller()
default:
return nil
}
}

func (i *installerImpl) setupPackage(ctx context.Context, pkg string) error {
switch pkg {
case packageDatadogInstaller:
Expand Down Expand Up @@ -344,3 +381,11 @@ func checkAvailableDiskSpace(pkg *oci.DownloadedPackage, path string) error {
}
return nil
}

func ensurePackageDirExists() error {
err := os.MkdirAll(PackagesPath, 0755)
if err != nil {
return fmt.Errorf("error creating packages directory: %w", err)
}
return nil
}
4 changes: 4 additions & 0 deletions pkg/fleet/installer/installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/assert"

"github.com/DataDog/datadog-agent/pkg/fleet/installer/repository"
"github.com/DataDog/datadog-agent/pkg/fleet/internal/db"
"github.com/DataDog/datadog-agent/pkg/fleet/internal/fixtures"
"github.com/DataDog/datadog-agent/pkg/fleet/internal/oci"
)
Expand All @@ -31,8 +32,11 @@ type testPackageManager struct {

func newTestPackageManager(t *testing.T, s *fixtures.Server, rootPath string, locksPath string) *testPackageManager {
repositories := repository.NewRepositories(rootPath, locksPath)
db, err := db.New(filepath.Join(rootPath, "packages.db"))
assert.NoError(t, err)
return &testPackageManager{
installerImpl{
db: db,
downloader: oci.NewDownloader(s.Client(), "", oci.RegistryAuthDefault),
repositories: repositories,
configsDir: t.TempDir(),
Expand Down
10 changes: 0 additions & 10 deletions pkg/fleet/installer/service/datadog_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,6 @@ const (

var installerUnits = []string{installerUnit, installerUnitExp}

// PreSetupInstaller creates the necessary directories for the installer to be installed.
// FIXME: This is a preinst and I feel bad about it
func PreSetupInstaller() error {
err := os.MkdirAll("/opt/datadog-packages", 0755)
if err != nil {
return fmt.Errorf("error creating /opt/datadog-packages: %w", err)
}
return nil
}

func addDDAgentUser(ctx context.Context) error {
if _, err := user.Lookup("dd-agent"); err == nil {
return nil
Expand Down