diff --git a/comp/core/workloadmeta/collectors/internal/podman/podman.go b/comp/core/workloadmeta/collectors/internal/podman/podman.go index ba7f13f7dcd8a9..a5b3b23a68e6ec 100644 --- a/comp/core/workloadmeta/collectors/internal/podman/podman.go +++ b/comp/core/workloadmeta/collectors/internal/podman/podman.go @@ -11,6 +11,7 @@ package podman import ( "context" "errors" + "os" "sort" "strings" @@ -25,8 +26,10 @@ import ( ) const ( - collectorID = "podman" - componentName = "workloadmeta-podman" + collectorID = "podman" + componentName = "workloadmeta-podman" + defaultBoltDBPath = "/var/lib/containers/storage/libpod/bolt_state.db" + defaultSqlitePath = "/var/lib/containers/storage/db.sql" ) type podmanClient interface { @@ -63,7 +66,38 @@ func (c *collector) Start(_ context.Context, store workloadmeta.Component) error return dderrors.NewDisabled(componentName, "Podman not detected") } - c.client = podman.NewDBClient(config.Datadog.GetString("podman_db_path")) + var dbPath string + dbPath = config.Datadog.GetString("podman_db_path") + + // We verify the user-provided path exists to prevent the collector entering a failing loop. + if dbPath != "" && !dbIsAccessible(dbPath) { + return dderrors.NewDisabled(componentName, "podman_db_path is misconfigured/not accessible") + } + + // If dbPath is empty (default value of `podman_db_path`), attempts to use the default rootfull database (BoltDB first, then SQLite) as podman feature was detected (existence of /var/lib/containers/storage) + if dbPath == "" { + if dbIsAccessible(defaultBoltDBPath) { + log.Infof("Podman feature detected and podman_db_path not configured, defaulting to: %s", defaultBoltDBPath) + dbPath = defaultBoltDBPath + } else if dbIsAccessible(defaultSqlitePath) { + log.Infof("Podman feature detected and podman_db_path not configured, defaulting to: %s", defaultSqlitePath) + dbPath = defaultSqlitePath + } else { + // `/var/lib/containers/storage` exists but the Agent cannot list out its content. + return dderrors.NewDisabled(componentName, "Podman feature detected but the default location for the containers DB is not accessible") + } + } + + // As the containers database file is hard-coded in Podman (non-user customizable), the client to use is determined thanks to the file extension. + if strings.HasSuffix(dbPath, ".sql") { + log.Debugf("Using SQLite client for Podman DB as provided path ends with .sql") + c.client = podman.NewSQLDBClient(dbPath) + } else if strings.HasSuffix(dbPath, ".db") { + log.Debugf("Using BoltDB client for Podman DB as provided path ends with .db") + c.client = podman.NewDBClient(dbPath) + } else { + return dderrors.NewDisabled(componentName, "Podman detected but podman_db_path does not end in a known-format (.db or .sql)") + } c.store = store return nil @@ -270,3 +304,11 @@ func status(state podman.ContainerStatus) workloadmeta.ContainerStatus { return workloadmeta.ContainerStatusUnknown } + +// dbIsAccessible verifies whether or not the provided file is accessible by the Agent +func dbIsAccessible(dbPath string) bool { + if _, err := os.Stat(dbPath); err == nil { + return true + } + return false +} diff --git a/pkg/config/config_template.yaml b/pkg/config/config_template.yaml index 7a6494a28bb8bf..fb2276819bbab4 100644 --- a/pkg/config/config_template.yaml +++ b/pkg/config/config_template.yaml @@ -2857,10 +2857,11 @@ api_key: # # listen_address: /var/vcap/data/garden/garden.sock -## @param podman_db_path - string - optional - default: /var/lib/containers/storage/libpod/bolt_state.db +## @param podman_db_path - string - optional - default: "" +## @env DD_PODMAN_DB_PATH - string - optional - default: "" ## Settings for Podman DB that Datadog Agent collects container metrics. # -# podman_db_path: /var/lib/containers/storage/libpod/bolt_state.db +# podman_db_path: "" {{ end -}} {{- if .ClusterAgent }} diff --git a/pkg/config/env/environment_containers.go b/pkg/config/env/environment_containers.go index f5f186ff152dd7..7e1c267123e120 100644 --- a/pkg/config/env/environment_containers.go +++ b/pkg/config/env/environment_containers.go @@ -26,7 +26,7 @@ const ( defaultWindowsContainerdSocketPath = "//./pipe/containerd-containerd" defaultLinuxCrioSocket = "/var/run/crio/crio.sock" defaultHostMountPrefix = "/host" - defaultPodmanContainersStoragePath = "/var/lib/containers" + defaultPodmanContainersStoragePath = "/var/lib/containers/storage" unixSocketPrefix = "unix://" winNamedPipePrefix = "npipe://" @@ -66,7 +66,7 @@ func detectContainerFeatures(features FeatureMap, cfg model.Reader) { detectContainerd(features, cfg) detectAWSEnvironments(features, cfg) detectCloudFoundry(features, cfg) - detectPodman(features) + detectPodman(features, cfg) } func detectKubernetes(features FeatureMap, cfg model.Reader) { @@ -195,7 +195,12 @@ func detectCloudFoundry(features FeatureMap, cfg model.Reader) { } } -func detectPodman(features FeatureMap) { +func detectPodman(features FeatureMap, cfg model.Reader) { + podmanDbPath := cfg.GetString("podman_db_path") + if podmanDbPath != "" { + features[Podman] = struct{}{} + return + } for _, defaultPath := range getDefaultPodmanPaths() { if _, err := os.Stat(defaultPath); err == nil { features[Podman] = struct{}{} diff --git a/pkg/config/setup/config.go b/pkg/config/setup/config.go index fe40c02e6becbf..85419ff93ce6f4 100644 --- a/pkg/config/setup/config.go +++ b/pkg/config/setup/config.go @@ -633,7 +633,7 @@ func InitConfig(config pkgconfigmodel.Config) { config.BindEnvAndSetDefault("container_labels_as_tags", map[string]string{}) // Podman - config.BindEnvAndSetDefault("podman_db_path", "/var/lib/containers/storage/libpod/bolt_state.db") + config.BindEnvAndSetDefault("podman_db_path", "") // Kubernetes config.BindEnvAndSetDefault("kubernetes_kubelet_host", "") diff --git a/pkg/util/podman/sqlite_db_client.go b/pkg/util/podman/sqlite_db_client.go new file mode 100644 index 00000000000000..6b95f29ab96e1d --- /dev/null +++ b/pkg/util/podman/sqlite_db_client.go @@ -0,0 +1,114 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build podman + +package podman + +import ( + "database/sql" + "encoding/json" + "fmt" + "path/filepath" + + // SQLite backend for database/sql + _ "modernc.org/sqlite" + + "github.com/DataDog/datadog-agent/pkg/util/log" +) + +// Same strategy as for BoltDB : we do not need the full podman go package. +// This reduces the number of dependencies and the size of the ultimately shipped binary. +// +// The functions in this file have been copied from +// https://github.com/containers/podman/blob/v5.0.0/libpod/sqlite_state.go +// The code has been adapted a bit to our needs. The only functions of that file +// that we need are AllContainers() and NewSqliteState(). +// +// This code could break in future versions of Podman. This has been tried with +// v4.9.2 and v5.0.0. + +// SQLDBClient is a client for the podman's state database in the SQLite format. +type SQLDBClient struct { + DBPath string +} + +const ( + // Deal with timezone automatically. + sqliteOptionLocation = "_loc=auto" + // Read-only mode (https://www.sqlite.org/pragma.html#pragma_query_only) + sqliteOptionQueryOnly = "&_query_only=true" + // Make sure busy timeout is set to high value to keep retrying when the db is locked. + // Timeout is in ms, so set it to 100s to have enough time to retry the operations. + sqliteOptionBusyTimeout = "&_busy_timeout=100000" + + // Assembled sqlite options used when opening the database. + sqliteOptions = "?" + sqliteOptionLocation + sqliteOptionQueryOnly + sqliteOptionBusyTimeout +) + +// NewSQLDBClient returns a DB client that uses the DB stored in dbPath. +func NewSQLDBClient(dbPath string) *SQLDBClient { + return &SQLDBClient{ + DBPath: dbPath, + } +} + +// getDBCon opens a connection to the SQLite-backed state database. +// Note: original function comes from https://github.com/containers/podman/blob/e71ec6f1d94d2d97fb3afe08aae0d8adaf8bddf0/libpod/sqlite_state.go#L57-L96 +// It was adapted as we don't need to write any information to the DB. +func (client *SQLDBClient) getDBCon() (*sql.DB, error) { + conn, err := sql.Open("sqlite", filepath.Join(client.DBPath, sqliteOptions)) + if err != nil { + return nil, fmt.Errorf("opening sqlite database: %w", err) + } + return conn, nil +} + +// GetAllContainers retrieves all the containers in the database. +// We retrieve the state always. +func (client *SQLDBClient) GetAllContainers() ([]Container, error) { + var res []Container + + conn, err := client.getDBCon() + if err != nil { + return nil, err + } + defer func() { + if errClose := conn.Close(); errClose != nil { + log.Warnf("failed to close sqlite db: %q", err) + } + }() + + rows, err := conn.Query("SELECT ContainerConfig.JSON, ContainerState.JSON AS StateJSON FROM ContainerConfig INNER JOIN ContainerState ON ContainerConfig.ID = ContainerState.ID;") + if err != nil { + return nil, fmt.Errorf("retrieving all containers from database: %w", err) + } + defer rows.Close() + + for rows.Next() { + var configJSON, stateJSON string + if err := rows.Scan(&configJSON, &stateJSON); err != nil { + return nil, fmt.Errorf("scanning container from database: %w", err) + } + + ctr := new(Container) + ctr.Config = new(ContainerConfig) + ctr.State = new(ContainerState) + + if err := json.Unmarshal([]byte(configJSON), ctr.Config); err != nil { + return nil, fmt.Errorf("unmarshalling container config: %w", err) + } + if err := json.Unmarshal([]byte(stateJSON), ctr.State); err != nil { + return nil, fmt.Errorf("unmarshalling container %s state: %w", ctr.Config.ID, err) + } + + res = append(res, *ctr) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return res, nil +} diff --git a/releasenotes/notes/podman-sqlite-backend-support-8437c6d5254b39ef.yaml b/releasenotes/notes/podman-sqlite-backend-support-8437c6d5254b39ef.yaml new file mode 100644 index 00000000000000..c2fe41be5d244e --- /dev/null +++ b/releasenotes/notes/podman-sqlite-backend-support-8437c6d5254b39ef.yaml @@ -0,0 +1,6 @@ +--- +enhancements: + - | + Supports Podman newer versions (4.8+) using SQLite instead of BoltDB for the containers database backend. + Setting ``podman_db_path`` to the path with the ``db.sql`` file (e.g. ``/var/lib/containers/storage/db.sql``) will make the Datadog Agent use the SQLite format. + **Note**: If ``podman_db_path`` is not set (default), the Datadog Agent attempts to use the default file ``libpod/bolt_state.db`` and ``db.sql`` from ``/var/lib/containers/storage``. diff --git a/tasks/dogstatsd.py b/tasks/dogstatsd.py index f0eeee90b926f7..f050f70d033104 100644 --- a/tasks/dogstatsd.py +++ b/tasks/dogstatsd.py @@ -18,7 +18,7 @@ # constants DOGSTATSD_BIN_PATH = os.path.join(".", "bin", "dogstatsd") STATIC_BIN_PATH = os.path.join(".", "bin", "static") -MAX_BINARY_SIZE = 39 * 1024 +MAX_BINARY_SIZE = 42 * 1024 DOGSTATSD_TAG = "datadog/dogstatsd:master"