10
"github.com/containers/podman/v5/libpod/define"
11
"github.com/sirupsen/logrus"
14
type containerNode struct {
17
dependsOn []*containerNode
18
dependedOn []*containerNode
21
// ContainerGraph is a dependency graph based on a set of containers.
22
type ContainerGraph struct {
23
nodes map[string]*containerNode
24
noDepNodes []*containerNode
25
notDependedOnNodes map[string]*containerNode
28
// DependencyMap returns the dependency graph as map with the key being a
29
// container and the value being the containers the key depends on.
30
func (cg *ContainerGraph) DependencyMap() (dependencies map[*Container][]*Container) {
31
dependencies = make(map[*Container][]*Container)
32
for _, node := range cg.nodes {
33
dependsOn := make([]*Container, len(node.dependsOn))
34
for i, d := range node.dependsOn {
35
dependsOn[i] = d.container
37
dependencies[node.container] = dependsOn
42
// BuildContainerGraph builds a dependency graph based on the container slice.
43
func BuildContainerGraph(ctrs []*Container) (*ContainerGraph, error) {
44
graph := new(ContainerGraph)
45
graph.nodes = make(map[string]*containerNode)
46
graph.notDependedOnNodes = make(map[string]*containerNode)
48
// Start by building all nodes, with no edges
49
for _, ctr := range ctrs {
50
ctrNode := new(containerNode)
52
ctrNode.container = ctr
54
graph.nodes[ctr.ID()] = ctrNode
55
graph.notDependedOnNodes[ctr.ID()] = ctrNode
58
// Now add edges based on dependencies
59
for _, node := range graph.nodes {
60
deps := node.container.Dependencies()
61
for _, dep := range deps {
63
depNode, ok := graph.nodes[dep]
65
return nil, fmt.Errorf("container %s depends on container %s not found in input list: %w", node.id, dep, define.ErrNoSuchCtr)
68
// Add the dependent node to the node's dependencies
69
// And add the node to the dependent node's dependedOn
70
node.dependsOn = append(node.dependsOn, depNode)
71
depNode.dependedOn = append(depNode.dependedOn, node)
73
// The dependency now has something depending on it
74
delete(graph.notDependedOnNodes, dep)
77
// Maintain a list of nodes with no dependencies
78
// (no edges coming from them)
80
graph.noDepNodes = append(graph.noDepNodes, node)
84
// Need to do cycle detection
85
// We cannot start or stop if there are cyclic dependencies
86
cycle, err := detectCycles(graph)
90
return nil, fmt.Errorf("cycle found in container dependency graph: %w", define.ErrInternal)
96
// Detect cycles in a container graph using Tarjan's strongly connected
97
// components algorithm
98
// Return true if a cycle is found, false otherwise
99
func detectCycles(graph *ContainerGraph) (bool, error) {
100
type nodeInfo struct {
108
nodes := make(map[string]*nodeInfo)
109
stack := make([]*containerNode, 0, len(graph.nodes))
111
var strongConnect func(*containerNode) (bool, error)
112
strongConnect = func(node *containerNode) (bool, error) {
113
logrus.Debugf("Strongconnecting node %s", node.id)
115
info := new(nodeInfo)
120
nodes[node.id] = info
122
stack = append(stack, node)
126
logrus.Debugf("Pushed %s onto stack", node.id)
128
// Work through all nodes we point to
129
for _, successor := range node.dependsOn {
130
if _, ok := nodes[successor.id]; !ok {
131
logrus.Debugf("Recursing to successor node %s", successor.id)
133
cycle, err := strongConnect(successor)
140
successorInfo := nodes[successor.id]
141
if successorInfo.lowLink < info.lowLink {
142
info.lowLink = successorInfo.lowLink
145
successorInfo := nodes[successor.id]
146
if successorInfo.index < info.lowLink && successorInfo.onStack {
147
info.lowLink = successorInfo.index
152
if info.lowLink == info.index {
155
return false, fmt.Errorf("empty stack in detectCycles: %w", define.ErrInternal)
159
topOfStack := stack[l-1]
162
// Popped item is no longer on the stack, mark as such
163
topInfo, ok := nodes[topOfStack.id]
165
return false, fmt.Errorf("finding node info for %s: %w", topOfStack.id, define.ErrInternal)
167
topInfo.onStack = false
169
logrus.Debugf("Finishing node %s. Popped %s off stack", node.id, topOfStack.id)
171
// If the top of the stack is not us, we have found a
173
if topOfStack.id != node.id {
181
for id, node := range graph.nodes {
182
if _, ok := nodes[id]; !ok {
183
cycle, err := strongConnect(node)
195
// Visit a node on a container graph and start the container, or set an error if
196
// a dependency failed to start. if restart is true, startNode will restart the node instead of starting it.
197
func startNode(ctx context.Context, node *containerNode, setError bool, ctrErrors map[string]error, ctrsVisited map[string]bool, restart bool) {
198
// First, check if we have already visited the node
199
if ctrsVisited[node.id] {
203
// If setError is true, a dependency of us failed
204
// Mark us as failed and recurse
206
// Mark us as visited, and set an error
207
ctrsVisited[node.id] = true
208
ctrErrors[node.id] = fmt.Errorf("a dependency of container %s failed to start: %w", node.id, define.ErrCtrStateInvalid)
210
// Hit anyone who depends on us, and set errors on them too
211
for _, successor := range node.dependedOn {
212
startNode(ctx, successor, true, ctrErrors, ctrsVisited, restart)
218
// Have all our dependencies started?
219
// If not, don't visit the node yet
221
for _, dep := range node.dependsOn {
222
depsVisited = depsVisited && ctrsVisited[dep.id]
225
// Don't visit us yet, all dependencies are not up
226
// We'll hit the dependencies eventually, and when we do it will
231
// Going to try to start the container, mark us as visited
232
ctrsVisited[node.id] = true
236
// Check if dependencies are running
237
// Graph traversal means we should have started them
238
// But they could have died before we got here
239
// Does not require that the container be locked, we only need to lock
241
depsStopped, err := node.container.checkDependenciesRunning()
243
ctrErrors[node.id] = err
245
} else if len(depsStopped) > 0 {
246
// Our dependencies are not running
247
depsList := strings.Join(depsStopped, ",")
248
ctrErrors[node.id] = fmt.Errorf("the following dependencies of container %s are not running: %s: %w", node.id, depsList, define.ErrCtrStateInvalid)
252
// Lock before we start
253
node.container.lock.Lock()
255
// Sync the container to pick up current state
257
if err := node.container.syncContainer(); err != nil {
259
ctrErrors[node.id] = err
263
// Start the container (only if it is not running)
264
if !ctrErrored && len(node.container.config.InitContainerType) < 1 {
265
if !restart && node.container.state.State != define.ContainerStateRunning {
266
if err := node.container.initAndStart(ctx); err != nil {
268
ctrErrors[node.id] = err
271
if restart && node.container.state.State != define.ContainerStatePaused && node.container.state.State != define.ContainerStateUnknown {
272
if err := node.container.restartWithTimeout(ctx, node.container.config.StopTimeout); err != nil {
274
ctrErrors[node.id] = err
279
node.container.lock.Unlock()
281
// Recurse to anyone who depends on us and start them
282
for _, successor := range node.dependedOn {
283
startNode(ctx, successor, ctrErrored, ctrErrors, ctrsVisited, restart)
287
// Visit a node on the container graph and remove it, or set an error if it
288
// failed to remove. Only intended for use in pod removal; do *not* use when
289
// removing individual containers.
290
// All containers are assumed to be *UNLOCKED* on running this function.
291
// Container locks will be acquired as necessary.
292
// Pod and infraID are optional. If a pod is given it must be *LOCKED*.
293
func removeNode(ctx context.Context, node *containerNode, pod *Pod, force bool, timeout *uint, setError bool, ctrErrors map[string]error, ctrsVisited map[string]bool, ctrNamedVolumes map[string]*ContainerNamedVolume) {
294
// If we already visited this node, we're done.
295
if ctrsVisited[node.id] {
299
// Someone who depends on us failed.
300
// Mark us as failed and recurse.
302
ctrsVisited[node.id] = true
303
ctrErrors[node.id] = fmt.Errorf("a container that depends on container %s could not be removed: %w", node.id, define.ErrCtrStateInvalid)
305
// Hit anyone who depends on us, set errors there as well.
306
for _, successor := range node.dependsOn {
307
removeNode(ctx, successor, pod, force, timeout, true, ctrErrors, ctrsVisited, ctrNamedVolumes)
311
// Does anyone still depend on us?
312
// Cannot remove if true. Once all our dependencies have been removed,
313
// we will be removed.
314
for _, dep := range node.dependedOn {
315
// The container that depends on us hasn't been removed yet.
317
if ok := ctrsVisited[dep.id]; !ok {
322
// Going to try to remove the node, mark us as visited
323
ctrsVisited[node.id] = true
327
// Verify that all that depend on us are gone.
328
// Graph traversal should guarantee this is true, but this isn't that
329
// expensive, and it's better to be safe.
330
for _, dep := range node.dependedOn {
331
if _, err := node.container.runtime.GetContainer(dep.id); err == nil {
333
ctrErrors[node.id] = fmt.Errorf("a container that depends on container %s still exists: %w", node.id, define.ErrDepExists)
337
// Lock the container
338
node.container.lock.Lock()
340
// Gate all subsequent bits behind a ctrErrored check - we don't want to
341
// proceed if a previous step failed.
343
if err := node.container.syncContainer(); err != nil {
345
ctrErrors[node.id] = err
350
for _, vol := range node.container.config.NamedVolumes {
351
ctrNamedVolumes[vol.Name] = vol
354
if pod != nil && pod.state.InfraContainerID == node.id {
355
pod.state.InfraContainerID = ""
356
if err := pod.save(); err != nil {
358
ctrErrors[node.id] = fmt.Errorf("error removing infra container %s from pod %s: %w", node.id, pod.ID(), err)
370
if _, _, err := node.container.runtime.removeContainer(ctx, node.container, opts); err != nil {
372
ctrErrors[node.id] = err
376
node.container.lock.Unlock()
378
// Recurse to anyone who we depend on and remove them
379
for _, successor := range node.dependsOn {
380
removeNode(ctx, successor, pod, force, timeout, ctrErrored, ctrErrors, ctrsVisited, ctrNamedVolumes)