1+ package containers
2+
3+ import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "os"
8+ "os/exec"
9+ "path/filepath"
10+ "strings"
11+ "time"
12+
13+ "github.com/coroot/coroot-node-agent/common"
14+ "github.com/coroot/coroot-node-agent/proc"
15+ "github.com/coroot/logparser"
16+ "inet.af/netaddr"
17+ "k8s.io/klog/v2"
18+ )
19+
20+ const podmanTimeout = 30 * time .Second
21+
22+ var (
23+ podmanClient * PodmanClient
24+ )
25+
26+ // PodmanClient represents a client for interacting with Podman API
27+ type PodmanClient struct {
28+ socketPath string
29+ }
30+
31+ // runCmd runs a command with a timeout and returns its output
32+ func runCmd (command string , timeout time.Duration ) (string , error ) {
33+ ctx , cancel := context .WithTimeout (context .Background (), timeout )
34+ defer cancel ()
35+
36+ cmd := exec .CommandContext (ctx , "sh" , "-c" , command )
37+ output , err := cmd .CombinedOutput ()
38+ if err != nil {
39+ return "" , fmt .Errorf ("command failed: %w, output: %s" , err , string (output ))
40+ }
41+ return string (output ), nil
42+ }
43+
44+ // isPodmanAvailable checks if the podman command is available
45+ func isPodmanAvailable () bool {
46+ _ , err := exec .LookPath ("podman" )
47+ return err == nil
48+ }
49+
50+ // PodmanInit initializes the Podman client
51+ func PodmanInit () error {
52+ klog .Infof ("Initializing Podman support" )
53+
54+ // Try common Podman socket paths
55+ sockets := []string {
56+ "/run/podman/podman.sock" ,
57+ "/var/run/podman/podman.sock" ,
58+ proc .HostPath ("/run/podman/podman.sock" ),
59+ proc .HostPath ("/var/run/podman/podman.sock" ),
60+ }
61+
62+ for _ , socket := range sockets {
63+ // Check if the socket exists and is accessible
64+ if _ , err := os .Stat (socket ); err == nil {
65+ klog .Infof ("Found Podman socket at %s" , socket )
66+ podmanClient = & PodmanClient {
67+ socketPath : socket ,
68+ }
69+ break
70+ } else {
71+ klog .V (5 ).Infof ("Podman socket not found at %s: %v" , socket , err )
72+ }
73+ }
74+
75+ // If no socket was found, that's okay - we'll try other approaches
76+ if podmanClient == nil {
77+ klog .Infof ("No Podman socket found, will use CLI-based inspection" )
78+ } else {
79+ klog .Infof ("Using Podman socket at %s" , podmanClient .socketPath )
80+ }
81+
82+ return nil
83+ }
84+
85+ // readContainerConfigFromFile reads container configuration from filesystem
86+ func readContainerConfigFromFile (containerID string ) (* ContainerMetadata , error ) {
87+ // Try to read container config from the standard Podman location
88+ configPath := filepath .Join ("/var/lib/containers/storage/overlay-containers" , containerID , "userdata" , "config.json" )
89+ configPath = proc .HostPath (configPath )
90+
91+ if _ , err := os .Stat (configPath ); err != nil {
92+ return nil , fmt .Errorf ("config file not found: %w" , err )
93+ }
94+
95+ data , err := os .ReadFile (configPath )
96+ if err != nil {
97+ return nil , fmt .Errorf ("failed to read config file: %w" , err )
98+ }
99+
100+ // Parse the container configuration
101+ var config struct {
102+ ID string `json:"id"`
103+ Name string `json:"name"`
104+ Image string `json:"rootfsImageName"`
105+ Config struct {
106+ Labels map [string ]string `json:"Labels"`
107+ Env []string `json:"Env"`
108+ } `json:"config"`
109+ Mounts []struct {
110+ Source string `json:"source"`
111+ Destination string `json:"destination"`
112+ } `json:"mounts"`
113+ NetworkSettings struct {
114+ Ports map [string ][]struct {
115+ HostIP string `json:"HostIp"`
116+ HostPort string `json:"HostPort"`
117+ } `json:"ports"`
118+ } `json:"networkSettings"`
119+ }
120+
121+ if err := json .Unmarshal (data , & config ); err != nil {
122+ return nil , fmt .Errorf ("failed to parse container config: %w" , err )
123+ }
124+
125+ res := & ContainerMetadata {
126+ name : strings .TrimPrefix (config .Name , "/" ),
127+ labels : config .Config .Labels ,
128+ image : config .Image ,
129+ volumes : map [string ]string {},
130+ hostListens : map [string ][]netaddr.IPPort {},
131+ networks : map [string ]ContainerNetwork {},
132+ env : map [string ]string {},
133+ }
134+
135+ // Parse volumes
136+ for _ , mount := range config .Mounts {
137+ res .volumes [mount .Destination ] = common .ParseKubernetesVolumeSource (mount .Source )
138+ }
139+
140+ // Parse environment variables
141+ for _ , envVar := range config .Config .Env {
142+ parts := strings .SplitN (envVar , "=" , 2 )
143+ if len (parts ) == 2 {
144+ res .env [parts [0 ]] = parts [1 ]
145+ }
146+ }
147+
148+ // Parse network settings
149+ for port , bindings := range config .NetworkSettings .Ports {
150+ if len (bindings ) > 0 {
151+ ipport , err := netaddr .ParseIPPort (bindings [0 ].HostIP + ":" + bindings [0 ].HostPort )
152+ if err != nil {
153+ continue
154+ }
155+ res .hostListens [port ] = append (res .hostListens [port ], ipport )
156+ }
157+ }
158+
159+ // Try to find log file
160+ logPath := filepath .Join ("/var/lib/containers/storage/overlay-containers" , containerID , "userdata" , "ctr.log" )
161+ logPath = proc .HostPath (logPath )
162+ if _ , err := os .Stat (logPath ); err == nil {
163+ res .logPath = logPath
164+ res .logDecoder = logparser.DockerJsonDecoder {} // Podman uses same log format as Docker
165+ }
166+
167+ return res , nil
168+ }
169+
170+ // PodmanInspect inspects a container using multiple approaches
171+ func PodmanInspect (containerID string ) (* ContainerMetadata , error ) {
172+ klog .Infof ("Inspecting Podman container %s" , containerID )
173+
174+ // First, try to read from filesystem directly
175+ if md , err := readContainerConfigFromFile (containerID ); err == nil {
176+ klog .Infof ("Successfully read container metadata from filesystem for %s" , containerID )
177+ return md , nil
178+ } else {
179+ klog .V (5 ).Infof ("Failed to read container metadata from filesystem for %s: %v" , containerID , err )
180+ }
181+
182+ // If filesystem read fails, try using podman command if available
183+ if isPodmanAvailable () {
184+ klog .Infof ("Trying to get container metadata using podman command for %s" , containerID )
185+ // Run podman inspect command
186+ cmd := fmt .Sprintf ("podman inspect %s" , containerID )
187+ output , err := runCmd (cmd , podmanTimeout )
188+ if err != nil {
189+ klog .Warningf ("Failed to run podman inspect for %s: %v" , containerID , err )
190+ return nil , fmt .Errorf ("failed to run podman inspect: %w" , err )
191+ }
192+
193+ // Parse the JSON output
194+ var containers []struct {
195+ ID string `json:"Id"`
196+ Name string `json:"Name"`
197+ Image string `json:"Image"`
198+ Config struct {
199+ Labels map [string ]string `json:"Labels"`
200+ Env []string `json:"Env"`
201+ } `json:"Config"`
202+ Mounts []struct {
203+ Source string `json:"Source"`
204+ Destination string `json:"Destination"`
205+ } `json:"Mounts"`
206+ NetworkSettings struct {
207+ Ports map [string ][]struct {
208+ HostIP string `json:"HostIp"`
209+ HostPort string `json:"HostPort"`
210+ } `json:"Ports"`
211+ } `json:"NetworkSettings"`
212+ }
213+
214+ if err := json .Unmarshal ([]byte (output ), & containers ); err != nil {
215+ klog .Warningf ("Failed to parse podman inspect output for %s: %v" , containerID , err )
216+ return nil , fmt .Errorf ("failed to parse podman inspect output: %w" , err )
217+ }
218+
219+ if len (containers ) == 0 {
220+ klog .Warningf ("No container found with ID %s" , containerID )
221+ return nil , fmt .Errorf ("no container found with ID %s" , containerID )
222+ }
223+
224+ container := containers [0 ]
225+ klog .Infof ("Successfully inspected container %s with name %s" , container .ID , container .Name )
226+
227+ res := & ContainerMetadata {
228+ name : strings .TrimPrefix (container .Name , "/" ),
229+ labels : container .Config .Labels ,
230+ image : container .Image ,
231+ volumes : map [string ]string {},
232+ hostListens : map [string ][]netaddr.IPPort {},
233+ networks : map [string ]ContainerNetwork {},
234+ env : map [string ]string {},
235+ }
236+
237+ // Parse volumes
238+ for _ , mount := range container .Mounts {
239+ res .volumes [mount .Destination ] = common .ParseKubernetesVolumeSource (mount .Source )
240+ }
241+
242+ // Parse environment variables
243+ for _ , envVar := range container .Config .Env {
244+ parts := strings .SplitN (envVar , "=" , 2 )
245+ if len (parts ) == 2 {
246+ res .env [parts [0 ]] = parts [1 ]
247+ }
248+ }
249+
250+ // Parse network settings
251+ for port , bindings := range container .NetworkSettings .Ports {
252+ if len (bindings ) > 0 {
253+ ipport , err := netaddr .ParseIPPort (bindings [0 ].HostIP + ":" + bindings [0 ].HostPort )
254+ if err != nil {
255+ continue
256+ }
257+ res .hostListens [port ] = append (res .hostListens [port ], ipport )
258+ }
259+ }
260+
261+ // Try to find log file
262+ logPath := fmt .Sprintf ("/var/lib/containers/storage/overlay-containers/%s/userdata/ctr.log" , containerID )
263+ logPath = proc .HostPath (logPath )
264+ if _ , err := os .Stat (logPath ); err == nil {
265+ res .logPath = logPath
266+ res .logDecoder = logparser.DockerJsonDecoder {} // Podman uses same log format as Docker
267+ }
268+
269+ return res , nil
270+ }
271+
272+ // If both approaches fail, return minimal metadata
273+ klog .Warningf ("Unable to get detailed metadata for Podman container %s, returning minimal metadata" , containerID )
274+ return & ContainerMetadata {
275+ name : fmt .Sprintf ("libpod-%s" , containerID [:12 ]),
276+ image : "unknown" ,
277+ labels : map [string ]string {},
278+ volumes : map [string ]string {},
279+ env : map [string ]string {},
280+ }, nil
281+ }
0 commit comments