From a4dedbed1dbf988381abee1e17d460fbea509137 Mon Sep 17 00:00:00 2001 From: David Desmarais-Michaud Date: Thu, 5 Mar 2026 20:14:53 -0500 Subject: [PATCH 1/2] atc: move checksum checks into admission control --- cmd/atc/flight_test.go | 56 +++++++++++++ cmd/atc/handler.go | 4 + cmd/atc/main_test.go | 121 +++++++++++++++++++++++++++- internal/atc/reconciler_flight.go | 2 + internal/atc/reconciler_instance.go | 2 + pkg/yoke/yoke_takeoff.go | 6 +- 6 files changed, 186 insertions(+), 5 deletions(-) diff --git a/cmd/atc/flight_test.go b/cmd/atc/flight_test.go index 12b0403e..af12c997 100644 --- a/cmd/atc/flight_test.go +++ b/cmd/atc/flight_test.go @@ -283,3 +283,59 @@ func TestNotAllowedFlightWasmURL(t *testing.T) { ) require.ErrorContains(t, err, `admission webhook "flights.yoke.cd" denied the request: module "http://localhost/basic.wasm" not allowed`) } + +func TestFlightInvalidChecksum(t *testing.T) { + client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) + require.NoError(t, err) + + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + + // Create the flight initially so that we can warm the cache. + // Otherwise it'll behave different when we run this test alone versus with the rest of the test suite. + // Either it'll fail on first load if never loaded before, or at takeoff during dry-run if preloaded. + // Here we are testing the preloaded case. + flight, err := flightIntf.Create( + context.Background(), + &v1alpha1.Flight{ + ObjectMeta: metav1.ObjectMeta{Name: "basic"}, + Spec: v1alpha1.FlightSpec{ + WasmURL: "http://wasmcache/basic.wasm", + Input: "{}", + }, + }, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + + defer func() { + require.NoError(t, flightIntf.Delete(context.Background(), flight.Name, metav1.DeleteOptions{})) + testutils.EventuallyNoErrorf( + t, + func() error { + if _, err := flightIntf.Get(context.Background(), flight.Name, metav1.GetOptions{}); !kerrors.IsNotFound(err) { + return fmt.Errorf("expected error not found but got: %w", err) + } + return nil + }, + time.Second, + 30*time.Second, + "expected flight to be removed from cluster but was not", + ) + }() + + flight.Spec.Checksum = "applebottomjeans" + + require.ErrorContains( + t, + retry.RetryOnConflict(retry.DefaultRetry, func() error { + flight, err := flightIntf.Get(context.Background(), flight.Name, metav1.GetOptions{}) + if err != nil { + return err + } + flight.Spec.Checksum = "applebottomjeans" + _, err = flightIntf.Update(context.Background(), flight, metav1.UpdateOptions{}) + return err + }), + `cannot verify module against expected checksum: wanted "applebottomjeans"`, + ) +} diff --git a/cmd/atc/handler.go b/cmd/atc/handler.go index 7aef1816..719fbb27 100644 --- a/cmd/atc/handler.go +++ b/cmd/atc/handler.go @@ -283,12 +283,14 @@ func Handler(params HandlerParams) http.Handler { takeoffParams := yoke.TakeoffParams{ Release: atc.ReleaseName(&cr), Namespace: cmp.Or(cr.GetNamespace(), "default"), + Checksum: airway.Spec.WasmURLs.FlightChecksum, CrossNamespace: airway.Spec.Template.Scope == apiextv1.ClusterScoped, ClusterAccess: host.ClusterAccessParams{ Enabled: airway.Spec.ClusterAccess, ResourceMatchers: airway.Spec.ResourceAccessMatchers, }, Flight: yoke.FlightParams{ + Path: airway.Spec.WasmURLs.Flight, Input: bytes.NewReader(data), MaxMemoryMib: uint64(airway.Spec.MaxMemoryMib), Timeout: airway.Spec.Timeout.Duration, @@ -766,7 +768,9 @@ func Handler(params HandlerParams) http.Handler { yoke.TakeoffParams{ Release: flight.Name, Namespace: flight.Namespace, + Checksum: flight.Spec.Checksum, Flight: yoke.FlightParams{ + Path: flight.Spec.WasmURL, Module: yoke.Module{ Instance: mod, SourceMetadata: yoke.ModuleSourcetadata{ diff --git a/cmd/atc/main_test.go b/cmd/atc/main_test.go index 25f061d4..848df458 100644 --- a/cmd/atc/main_test.go +++ b/cmd/atc/main_test.go @@ -2564,7 +2564,7 @@ func TestStatusUpdates(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - backendIntf.Delete(ctx, "test", metav1.DeleteOptions{}) + _ = backendIntf.Delete(ctx, "test", metav1.DeleteOptions{}) testutils.EventuallyNoErrorf( t, @@ -3707,7 +3707,7 @@ func TestInvalidFlightURL(t *testing.T) { func() error { _, err := client.AirwayIntf.Get(context.Background(), airway.Name, metav1.GetOptions{}) if !kerrors.IsNotFound(err) { - return fmt.Errorf("expecte error not found but got: %v", err) + return fmt.Errorf("expected error not found but got: %v", err) } return nil }, @@ -3769,3 +3769,120 @@ func TestInvalidFlightURL(t *testing.T) { }) require.ErrorContains(t, err, `admission webhook "backends.examples.com" denied the request: module "http://localhost/evilmodule" not allowed`) } + +func TestInvalidChecksum(t *testing.T) { + client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) + require.NoError(t, err) + + airway := &v1alpha1.Airway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backends.examples.com", + }, + Spec: v1alpha1.AirwaySpec{ + WasmURLs: v1alpha1.WasmURLs{ + Flight: "http://wasmcache/flight.v1.wasm", + }, + Mode: v1alpha1.AirwayModeSubscription, + ClusterAccess: true, + Template: apiextv1.CustomResourceDefinitionSpec{ + Group: "examples.com", + Names: apiextv1.CustomResourceDefinitionNames{ + Plural: "backends", + Singular: "backend", + Kind: "Backend", + }, + Scope: apiextv1.NamespaceScoped, + Versions: []apiextv1.CustomResourceDefinitionVersion{ + { + Name: "v1", + Served: true, + Storage: true, + Schema: &apiextv1.CustomResourceValidation{ + OpenAPIV3Schema: openapi.SchemaFor[backendv1.Backend](), + }, + }, + }, + }, + }, + } + + airway, err = client.AirwayIntf.Create(context.Background(), airway, metav1.CreateOptions{}) + require.NoError(t, err) + + defer func() { + require.NoError(t, client.AirwayIntf.Delete(context.Background(), airway.Name, metav1.DeleteOptions{})) + testutils.EventuallyNoErrorf( + t, + func() error { + if _, err := client.AirwayIntf.Get(context.Background(), airway.Name, metav1.GetOptions{}); !kerrors.IsNotFound(err) { + return fmt.Errorf("expected error to be not found but got: %w", err) + } + return nil + }, + time.Second, + 30*time.Second, + "expected airway to be deleted proper", + ) + }() + + require.NoError(t, + client.WaitForReady(context.Background(), internal.Must2(internal.ToUnstructured(airway)), k8s.WaitOptions{ + Timeout: 30 * time.Second, + Interval: time.Second, + }), + ) + + backendGVR := schema.GroupVersionResource{ + Group: "examples.com", + Version: "v1", + Resource: "backends", + } + + backendIntf := k8s.TypedInterface[backendv1.Backend](client.Dynamic, backendGVR).Namespace("default") + + be, err := backendIntf.Create( + context.Background(), + &backendv1.Backend{ + TypeMeta: metav1.TypeMeta{ + Kind: "Backend", + APIVersion: "examples.com/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: backendv1.BackendSpec{ + Image: "yokecd/c4ts:latest", + Replicas: 1, + }, + }, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + + require.NoError( + t, + retry.RetryOnConflict(retry.DefaultBackoff, func() error { + airway, err := client.AirwayIntf.Get(context.Background(), airway.Name, metav1.GetOptions{}) + if err != nil { + return err + } + airway.Spec.WasmURLs.FlightChecksum = "applebottomjeans" + _, err = client.AirwayIntf.Update(context.Background(), airway, metav1.UpdateOptions{}) + return err + }), + ) + + require.ErrorContains( + t, + retry.RetryOnConflict(retry.DefaultBackoff, func() error { + be, err := backendIntf.Get(context.Background(), be.Name, metav1.GetOptions{}) + if err != nil { + return err + } + be.Spec.Replicas += 1 + _, err = backendIntf.Update(context.Background(), be, metav1.UpdateOptions{}) + return err + }), + `cannot verify module against expected checksum: wanted "applebottomjeans"`, + ) +} diff --git a/internal/atc/reconciler_flight.go b/internal/atc/reconciler_flight.go index 2a781de4..b67581a2 100644 --- a/internal/atc/reconciler_flight.go +++ b/internal/atc/reconciler_flight.go @@ -153,7 +153,9 @@ func flightReconciler(modules *cache.ModuleCache, clusterScope bool) ctrl.Funcs ReleasePrefix: releasePrefix, Release: flight.Name, Namespace: flight.Namespace, + Checksum: flight.Spec.Checksum, Flight: yoke.FlightParams{ + Path: flight.Spec.WasmURL, Module: yoke.Module{ Instance: mod, SourceMetadata: internal.Source{ diff --git a/internal/atc/reconciler_instance.go b/internal/atc/reconciler_instance.go index 3c68666d..f1044fb7 100644 --- a/internal/atc/reconciler_instance.go +++ b/internal/atc/reconciler_instance.go @@ -350,7 +350,9 @@ func (atc atc) InstanceReconciler(params InstanceReconcilerParams) ctrl.Funcs { takeoffParams := yoke.TakeoffParams{ Release: release, Namespace: event.Namespace, + Checksum: params.Airway.Spec.WasmURLs.FlightChecksum, Flight: yoke.FlightParams{ + Path: params.Airway.Spec.WasmURLs.Flight, Input: bytes.NewReader(data), Timeout: params.Airway.Spec.Timeout.Duration, }, diff --git a/pkg/yoke/yoke_takeoff.go b/pkg/yoke/yoke_takeoff.go index 283c7442..794df7dd 100644 --- a/pkg/yoke/yoke_takeoff.go +++ b/pkg/yoke/yoke_takeoff.go @@ -348,7 +348,7 @@ func (commander Commander) Takeoff(ctx context.Context, params TakeoffParams) (e fullReleaseName := params.ReleasePrefix + params.Release source := func() internal.Source { - if params.Flight.Path == "" { + if params.Flight.Module.Instance != nil { return params.Flight.Module.SourceMetadata } return internal.SourceFrom(params.Flight.Path, params.Flight.Wasm) @@ -360,10 +360,10 @@ func (commander Commander) Takeoff(ctx context.Context, params TakeoffParams) (e } if internal.IsPinnableReference(source.Ref) { - if _, ok := internal.Find(release.History, func(revision internal.Revision) bool { + if rev, ok := internal.Find(release.History, func(revision internal.Revision) bool { return revision.Source.Ref == source.Ref && revision.Source.Checksum != source.Checksum }); ok { - return fmt.Errorf("module %q has changed since last use", source.Ref) + return fmt.Errorf("module %q has changed since last use: got %q but wanted %q", source.Ref, source.Checksum, rev.Source.Checksum) } } From d051d27974ef4a247d7a3581e37b6b857af168ed Mon Sep 17 00:00:00 2001 From: David Desmarais-Michaud Date: Sat, 7 Mar 2026 16:53:37 -0500 Subject: [PATCH 2/2] atc/test: dump atc logs on atc integration test failures --- cmd/atc/main_test.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cmd/atc/main_test.go b/cmd/atc/main_test.go index 848df458..027d9811 100644 --- a/cmd/atc/main_test.go +++ b/cmd/atc/main_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "os" "slices" "strconv" @@ -143,7 +144,24 @@ func TestMain(m *testing.M) { Poll: time.Second, })) - os.Exit(m.Run()) + exitCode := m.Run() + + if exitCode != 0 { + podintf := client.Clientset.CoreV1().Pods("atc") + list, err := podintf.List(context.Background(), metav1.ListOptions{LabelSelector: "yoke.cd/app=atc"}) + if err != nil { + panic(err) + } + logs, err := podintf.GetLogs(list.Items[0].Name, &corev1.PodLogOptions{}).Stream(context.Background()) + if err != nil { + panic(err) + } + if _, err := io.Copy(os.Stdout, logs); err != nil { + panic(err) + } + } + + os.Exit(exitCode) } func TestAirTrafficController(t *testing.T) {