Add ContainerIter utility for ranging over pod containers

This commit is contained in:
Tim Allclair 2025-03-27 11:11:42 -07:00
parent b15dfce6cb
commit 5928fc0e60
4 changed files with 346 additions and 38 deletions

View file

@ -17,6 +17,7 @@ limitations under the License.
package pod
import (
"iter"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -60,30 +61,42 @@ type ContainerVisitor func(container *api.Container, containerType ContainerType
// visiting is short-circuited. VisitContainers returns true if visiting completes,
// false if visiting was short-circuited.
func VisitContainers(podSpec *api.PodSpec, mask ContainerType, visitor ContainerVisitor) bool {
if mask&InitContainers != 0 {
for i := range podSpec.InitContainers {
if !visitor(&podSpec.InitContainers[i], InitContainers) {
return false
}
}
}
if mask&Containers != 0 {
for i := range podSpec.Containers {
if !visitor(&podSpec.Containers[i], Containers) {
return false
}
}
}
if mask&EphemeralContainers != 0 {
for i := range podSpec.EphemeralContainers {
if !visitor((*api.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) {
return false
}
for c, t := range ContainerIter(podSpec, mask) {
if !visitor(c, t) {
return false
}
}
return true
}
// ContainerIter returns an iterator over all containers in the given pod spec with a masked type.
// The iteration order is InitContainers, then main Containers, then EphemeralContainers.
func ContainerIter(podSpec *api.PodSpec, mask ContainerType) iter.Seq2[*api.Container, ContainerType] {
return func(yield func(*api.Container, ContainerType) bool) {
if mask&InitContainers != 0 {
for i := range podSpec.InitContainers {
if !yield(&podSpec.InitContainers[i], InitContainers) {
return
}
}
}
if mask&Containers != 0 {
for i := range podSpec.Containers {
if !yield(&podSpec.Containers[i], Containers) {
return
}
}
}
if mask&EphemeralContainers != 0 {
for i := range podSpec.EphemeralContainers {
if !yield((*api.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) {
return
}
}
}
}
}
// Visitor is called with each object name, and returns true if visiting should continue
type Visitor func(name string) (shouldContinue bool)

View file

@ -208,6 +208,147 @@ func TestVisitContainers(t *testing.T) {
}
}
func TestContainerIter(t *testing.T) {
testCases := []struct {
desc string
spec *api.PodSpec
wantContainers []string
mask ContainerType
}{
{
desc: "empty podspec",
spec: &api.PodSpec{},
wantContainers: []string{},
mask: AllContainers,
},
{
desc: "regular containers",
spec: &api.PodSpec{
Containers: []api.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []api.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []api.EphemeralContainer{
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"c1", "c2"},
mask: Containers,
},
{
desc: "init containers",
spec: &api.PodSpec{
Containers: []api.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []api.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []api.EphemeralContainer{
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"i1", "i2"},
mask: InitContainers,
},
{
desc: "init + main containers",
spec: &api.PodSpec{
Containers: []api.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []api.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []api.EphemeralContainer{
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"i1", "i2", "c1", "c2"},
mask: InitContainers | Containers,
},
{
desc: "ephemeral containers",
spec: &api.PodSpec{
Containers: []api.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []api.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []api.EphemeralContainer{
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"e1", "e2"},
mask: EphemeralContainers,
},
{
desc: "all container types",
spec: &api.PodSpec{
Containers: []api.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []api.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []api.EphemeralContainer{
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"},
mask: AllContainers,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
gotContainers := []string{}
for c, containerType := range ContainerIter(tc.spec, tc.mask) {
gotContainers = append(gotContainers, c.Name)
switch containerType {
case InitContainers:
if c.Name[0] != 'i' {
t.Errorf("ContainerIter() yielded container type InitContainers for container %q", c.Name)
}
case Containers:
if c.Name[0] != 'c' {
t.Errorf("ContainerIter() yielded container type Containers for container %q", c.Name)
}
case EphemeralContainers:
if c.Name[0] != 'e' {
t.Errorf("ContainerIter() yielded container type EphemeralContainers for container %q", c.Name)
}
default:
t.Errorf("ContainerIter() yielded unknown container type %d", containerType)
}
}
if !cmp.Equal(gotContainers, tc.wantContainers) {
t.Errorf("ContainerIter() = %+v, want %+v", gotContainers, tc.wantContainers)
}
})
}
}
func TestPodSecrets(t *testing.T) {
// Stub containing all possible secret references in a pod.
// The names of the referenced secrets match struct paths detected by reflection.

View file

@ -18,6 +18,7 @@ package pod
import (
"fmt"
"iter"
"time"
v1 "k8s.io/api/core/v1"
@ -105,30 +106,42 @@ func skipEmptyNames(visitor Visitor) Visitor {
// visiting is short-circuited. VisitContainers returns true if visiting completes,
// false if visiting was short-circuited.
func VisitContainers(podSpec *v1.PodSpec, mask ContainerType, visitor ContainerVisitor) bool {
if mask&InitContainers != 0 {
for i := range podSpec.InitContainers {
if !visitor(&podSpec.InitContainers[i], InitContainers) {
return false
}
}
}
if mask&Containers != 0 {
for i := range podSpec.Containers {
if !visitor(&podSpec.Containers[i], Containers) {
return false
}
}
}
if mask&EphemeralContainers != 0 {
for i := range podSpec.EphemeralContainers {
if !visitor((*v1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) {
return false
}
for c, t := range ContainerIter(podSpec, mask) {
if !visitor(c, t) {
return false
}
}
return true
}
// ContainerIter returns an iterator over all containers in the given pod spec with a masked type.
// The iteration order is InitContainers, then main Containers, then EphemeralContainers.
func ContainerIter(podSpec *v1.PodSpec, mask ContainerType) iter.Seq2[*v1.Container, ContainerType] {
return func(yield func(*v1.Container, ContainerType) bool) {
if mask&InitContainers != 0 {
for i := range podSpec.InitContainers {
if !yield(&podSpec.InitContainers[i], InitContainers) {
return
}
}
}
if mask&Containers != 0 {
for i := range podSpec.Containers {
if !yield(&podSpec.Containers[i], Containers) {
return
}
}
}
if mask&EphemeralContainers != 0 {
for i := range podSpec.EphemeralContainers {
if !yield((*v1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) {
return
}
}
}
}
}
// VisitPodSecretNames invokes the visitor function with the name of every secret
// referenced by the pod spec. If visitor returns false, visiting is short-circuited.
// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited.

View file

@ -426,6 +426,147 @@ func TestVisitContainers(t *testing.T) {
}
}
func TestContainerIter(t *testing.T) {
testCases := []struct {
desc string
spec *v1.PodSpec
wantContainers []string
mask ContainerType
}{
{
desc: "empty podspec",
spec: &v1.PodSpec{},
wantContainers: []string{},
mask: AllContainers,
},
{
desc: "regular containers",
spec: &v1.PodSpec{
Containers: []v1.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []v1.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []v1.EphemeralContainer{
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"c1", "c2"},
mask: Containers,
},
{
desc: "init containers",
spec: &v1.PodSpec{
Containers: []v1.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []v1.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []v1.EphemeralContainer{
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"i1", "i2"},
mask: InitContainers,
},
{
desc: "init + main containers",
spec: &v1.PodSpec{
Containers: []v1.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []v1.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []v1.EphemeralContainer{
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"i1", "i2", "c1", "c2"},
mask: InitContainers | Containers,
},
{
desc: "ephemeral containers",
spec: &v1.PodSpec{
Containers: []v1.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []v1.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []v1.EphemeralContainer{
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"e1", "e2"},
mask: EphemeralContainers,
},
{
desc: "all container types",
spec: &v1.PodSpec{
Containers: []v1.Container{
{Name: "c1"},
{Name: "c2"},
},
InitContainers: []v1.Container{
{Name: "i1"},
{Name: "i2"},
},
EphemeralContainers: []v1.EphemeralContainer{
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
},
},
wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"},
mask: AllContainers,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
gotContainers := []string{}
for c, containerType := range ContainerIter(tc.spec, tc.mask) {
gotContainers = append(gotContainers, c.Name)
switch containerType {
case InitContainers:
if c.Name[0] != 'i' {
t.Errorf("ContainerIter() yielded container type InitContainers for container %q", c.Name)
}
case Containers:
if c.Name[0] != 'c' {
t.Errorf("ContainerIter() yielded container type Containers for container %q", c.Name)
}
case EphemeralContainers:
if c.Name[0] != 'e' {
t.Errorf("ContainerIter() yielded container type EphemeralContainers for container %q", c.Name)
}
default:
t.Errorf("ContainerIter() yielded unknown container type %d", containerType)
}
}
if !cmp.Equal(gotContainers, tc.wantContainers) {
t.Errorf("ContainerIter() = %+v, want %+v", gotContainers, tc.wantContainers)
}
})
}
}
func TestPodSecrets(t *testing.T) {
// Stub containing all possible secret references in a pod.
// The names of the referenced secrets match struct paths detected by reflection.