From 8cee96515043712dc2caca04aea7475fa78a2506 Mon Sep 17 00:00:00 2001 From: David Desmarais-Michaud Date: Mon, 29 Apr 2024 22:30:26 -0400 Subject: [PATCH] yoke: ensure namespace exists before applying resources --- cmd/yoke/cmd_takeoff.go | 1 + cmd/yoke/main_test.go | 31 +++++++++++++++++++++++++++++++ internal/k8s/k8s.go | 20 ++++++++++++++++++++ internal/wasi/wasi.go | 9 ++++----- pkg/yoke/yoke.go | 7 +++++++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/cmd/yoke/cmd_takeoff.go b/cmd/yoke/cmd_takeoff.go index 460f281..e91dff9 100644 --- a/cmd/yoke/cmd_takeoff.go +++ b/cmd/yoke/cmd_takeoff.go @@ -135,6 +135,7 @@ func TakeOff(ctx context.Context, params TakeoffParams) error { Release: params.Release, Resources: resources, FlightID: params.Flight.Path, + Namespace: params.Flight.Namespace, Wasm: wasm, SkipDryRun: params.SkipDryRun, ForceConflicts: params.ForceConflicts, diff --git a/cmd/yoke/main_test.go b/cmd/yoke/main_test.go index dc14678..faf5c59 100644 --- a/cmd/yoke/main_test.go +++ b/cmd/yoke/main_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/require" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -148,3 +149,33 @@ func TestReleaseOwnership(t *testing.T) { `failed to validate ownership: conflict(s): resource "default.apps.v1.deployment.sample-app" is owned by release "foo"`, ) } + +func TestTakeoffWithNamespace(t *testing.T) { + rest, err := clientcmd.BuildConfigFromFlags("", home.Kubeconfig) + require.NoError(t, err) + + client, err := kubernetes.NewForConfig(rest) + require.NoError(t, err) + + _, err = client.CoreV1().Namespaces().Get(context.Background(), "test-ns", metav1.GetOptions{}) + require.True(t, kerrors.IsNotFound(err)) + + settings := GlobalSettings{KubeConfigPath: home.Kubeconfig} + + params := TakeoffParams{ + Release: "foo", + GlobalSettings: settings, + Flight: TakeoffFlightParams{ + Input: createBasicDeployment(t, "sample-app", "default"), + Namespace: "test-ns", + }, + } + + require.NoError(t, TakeOff(context.Background(), params)) + defer func() { + require.NoError(t, Mayday(context.Background(), MaydayParams{Release: "foo", GlobalSettings: settings})) + }() + + _, err = client.CoreV1().Namespaces().Get(context.Background(), "test-ns", metav1.GetOptions{}) + require.NoError(t, err) +} diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go index 185492e..53dcba4 100644 --- a/internal/k8s/k8s.go +++ b/internal/k8s/k8s.go @@ -364,6 +364,26 @@ func (client Client) DeleteRevisions(ctx context.Context, release string) error Delete(ctx, releaseName(release), metav1.DeleteOptions{}) } +func (client Client) EnsureNamespace(ctx context.Context, namespace string) error { + defer internal.DebugTimer(ctx, "ensuring namespace")() + + if _, err := client.clientset.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return err + } + + if _, err := client.clientset.CoreV1().Namespaces().Create( + ctx, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, + metav1.CreateOptions{}, + ); err != nil { + return fmt.Errorf("failed to create namespace: %w", err) + } + } + + return nil +} + func IsNamespaced(resource dynamic.ResourceInterface) bool { _, ok := resource.(interface{ Namespace(string) bool }) return ok diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go index 3c29371..403959a 100644 --- a/internal/wasi/wasi.go +++ b/internal/wasi/wasi.go @@ -29,13 +29,12 @@ func Execute(ctx context.Context, params ExecParams) (output []byte, err error) NewRuntimeConfig(). WithCloseOnContextDone(true) - // Create a new WebAssembly Runtime. - wasi := wazero.NewRuntimeWithConfig(ctx, cfg) + runtime := wazero.NewRuntimeWithConfig(ctx, cfg) defer func() { - err = xerr.MultiErrFrom("", err, wasi.Close(ctx)) + err = xerr.MultiErrFrom("", err, runtime.Close(ctx)) }() - wasi_snapshot_preview1.MustInstantiate(ctx, wasi) + wasi_snapshot_preview1.MustInstantiate(ctx, runtime) var ( stdout bytes.Buffer @@ -60,7 +59,7 @@ func Execute(ctx context.Context, params ExecParams) (output []byte, err error) moduleCfg = moduleCfg.WithEnv(key, value) } - if _, err := wasi.InstantiateWithConfig(ctx, params.Wasm, moduleCfg); err != nil { + if _, err := runtime.InstantiateWithConfig(ctx, params.Wasm, moduleCfg); err != nil { details := stderr.String() if details == "" { details = "(no output captured on stderr)" diff --git a/pkg/yoke/yoke.go b/pkg/yoke/yoke.go index 2d1498f..d0c327d 100644 --- a/pkg/yoke/yoke.go +++ b/pkg/yoke/yoke.go @@ -31,6 +31,7 @@ type TakeoffParams struct { Release string Resources []*unstructured.Unstructured FlightID string + Namespace string Wasm []byte SkipDryRun bool ForceConflicts bool @@ -54,6 +55,12 @@ func (client Client) Takeoff(ctx context.Context, params TakeoffParams) error { return fmt.Errorf("failed to validate ownership: %w", err) } + if params.Namespace != "" { + if err := client.k8s.EnsureNamespace(ctx, params.Namespace); err != nil { + return fmt.Errorf("failed to ensure namespace: %w", err) + } + } + applyOpts := k8s.ApplyResourcesOpts{ SkipDryRun: params.SkipDryRun, ForceConflicts: params.ForceConflicts,