Skip to content

Commit

Permalink
[Podman] Supports SQLite containers database back-end to support newe…
Browse files Browse the repository at this point in the history
…r versions (4.8+) (#24373)

* go import + licenses + release note

* sqlite client + modifies wlm podman init

* new podman detection and config

* Doc suggestion on release note formatting

Co-authored-by: Alicia Scott <[email protected]>

* Use pure-go SQLite driver instead of CGo implementatioon

* Increase max_dsd binary size to 42 MB

---------

Co-authored-by: Alicia Scott <[email protected]>
  • Loading branch information
2 people authored and CelianR committed Apr 26, 2024
1 parent 2b3c7bd commit 5ea7b43
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 10 deletions.
48 changes: 45 additions & 3 deletions comp/core/workloadmeta/collectors/internal/podman/podman.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package podman
import (
"context"
"errors"
"os"
"sort"
"strings"

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions pkg/config/config_template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
11 changes: 8 additions & 3 deletions pkg/config/env/environment_containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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://"

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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{}{}
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/setup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
114 changes: 114 additions & 0 deletions pkg/util/podman/sqlite_db_client.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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``.
2 changes: 1 addition & 1 deletion tasks/dogstatsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down

0 comments on commit 5ea7b43

Please sign in to comment.