Files
Felis/internal/build/userns_test.go

145 lines
4.9 KiB
Go

package build
import (
"context"
"sync/atomic"
"testing"
"time"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
// "auto" follows the probe through every copy of the Config; "on" and "off"
// ignore it.
func TestUserNamespacesMode(t *testing.T) {
probe := new(atomic.Bool)
for _, tc := range []struct {
mode string
probe bool
want bool
}{
{"", false, false}, {"", true, true}, {UserNamespacesAuto, true, true},
{UserNamespacesOn, false, true}, {UserNamespacesOff, true, false},
} {
b, _, jb := newBuilder()
b.Config.UserNamespaces = tc.mode
b.Config.UserNamespacesProbe = probe
probe.Store(tc.probe)
if _, err := b.Submit(context.Background(), goodRequest()); err != nil {
t.Fatalf("Submit: %v", err)
}
if got := jb.created[0].UserNamespaces; got != tc.want {
t.Errorf("mode %q, probe %v: UserNamespaces = %v, want %v", tc.mode, tc.probe, got, tc.want)
}
}
b, _, jb := newBuilder()
if _, err := b.Submit(context.Background(), goodRequest()); err != nil || jb.created[0].UserNamespaces {
t.Errorf("no probe wired: err %v, UserNamespaces %v", err, jb.created[0].UserNamespaces)
}
}
// The probe pod has the shape that needs user-namespace support, and runs
// nothing that matters.
func TestUsernsProbeJobShape(t *testing.T) {
job := UsernsProbeJob("felis-build", "felis-build", "felis:test", "userns-probe-x")
spec := job.Spec.Template.Spec
if spec.HostUsers == nil || *spec.HostUsers {
t.Fatal("the probe must ask for hostUsers: false")
}
if spec.AutomountServiceAccountToken == nil || *spec.AutomountServiceAccountToken || spec.ServiceAccountName != "felis-build" {
t.Error("the probe must run as the bare build SA with no token")
}
c := spec.Containers[0]
if c.Image != "felis:test" || len(c.Args) != 1 || c.Args[0] != "version" {
t.Errorf("probe container runs %s %v", c.Image, c.Args)
}
if c.SecurityContext.RunAsUser == nil || *c.SecurityContext.RunAsUser != 0 || len(c.SecurityContext.Capabilities.Add) != 3 {
t.Error("the probe must run as root with kaniko's capabilities, the case user namespaces must carry")
}
if len(spec.Volumes) != 1 || spec.Volumes[0].EmptyDir == nil {
t.Error("the probe must mount an emptyDir, as build pods do")
}
if job.Labels[LabelManagedBy] != managedByValue {
t.Error("the probe must sit under the build egress policy")
}
}
func TestProbeUserNamespaces(t *testing.T) {
timeout, poll := usernsProbeTimeout, usernsProbePoll
t.Cleanup(func() { usernsProbeTimeout, usernsProbePoll = timeout, poll })
usernsProbePoll = 5 * time.Millisecond
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
for _, tc := range []struct {
name string
cond batchv1.JobConditionType
want bool
}{
{"complete", batchv1.JobComplete, true},
{"failed", batchv1.JobFailed, false},
{"never finishes", "", false},
} {
t.Run(tc.name, func(t *testing.T) {
// A finished probe must answer long before the timeout would.
usernsProbeTimeout = 5 * time.Second
if tc.cond == "" {
usernsProbeTimeout = 100 * time.Millisecond
}
start := time.Now()
cl := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&batchv1.Job{}).Build()
jobs := NewK8sJobs(cl, Config{})
type result struct {
ok bool
err error
}
done := make(chan result, 1)
go func() {
ok, err := jobs.ProbeUserNamespaces(context.Background(), "felis:test")
done <- result{ok, err}
}()
if tc.cond != "" {
finishProbe(t, cl, tc.cond)
}
r := <-done
if r.err != nil || r.ok != tc.want {
t.Fatalf("ProbeUserNamespaces = (%v, %v), want (%v, nil)", r.ok, r.err, tc.want)
}
if tc.cond != "" && time.Since(start) > 2*time.Second {
t.Fatalf("the probe answered after %s: it waited out the timeout instead of reading the verdict", time.Since(start))
}
var left batchv1.JobList
if err := cl.List(context.Background(), &left, client.InNamespace(defaultNamespace)); err != nil || len(left.Items) != 0 {
t.Fatalf("probe jobs left behind: %d (%v)", len(left.Items), err)
}
})
}
}
// finishProbe waits for the probe Job to appear and marks it finished.
func finishProbe(t *testing.T, cl client.Client, cond batchv1.JobConditionType) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
var jobs batchv1.JobList
if err := cl.List(context.Background(), &jobs, client.InNamespace(defaultNamespace)); err != nil {
t.Fatal(err)
}
if len(jobs.Items) == 1 {
job := jobs.Items[0]
job.Status.Conditions = []batchv1.JobCondition{{Type: cond, Status: corev1.ConditionTrue}}
if err := cl.Status().Update(context.Background(), &job); err != nil {
t.Fatal(err)
}
return
}
time.Sleep(2 * time.Millisecond)
}
t.Fatal("the probe job never appeared")
}