From 9f2e6ac18ec68389058be4e4b595250db3f568f3 Mon Sep 17 00:00:00 2001 From: jK <33685667+jithinkunjachan@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:24:39 +0200 Subject: [PATCH 1/2] feat: add CLI commands for fetching parent and descendant keys Signed-off-by: jK <33685667+jithinkunjachan@users.noreply.github.com> --- cli/cmd/get.go | 98 +-------- cli/cmd/key.go | 154 ++++++++++++++ cli/cmd/tenant.go | 101 +++++++++ integration/key_test.go | 290 ++++++++++++++++++++++++++ integration/registration_test.go | 1 - integration/setup_test.go | 57 ++++- integration/tenant_test.go | 10 +- pkg/api/v1/proto/admin/key_convert.go | 2 +- pkg/api/v1/proto/admin/key_service.go | 2 +- 9 files changed, 602 insertions(+), 113 deletions(-) create mode 100644 cli/cmd/key.go create mode 100644 cli/cmd/tenant.go create mode 100644 integration/key_test.go diff --git a/cli/cmd/get.go b/cli/cmd/get.go index f2159ab..3c0729e 100644 --- a/cli/cmd/get.go +++ b/cli/cmd/get.go @@ -1,16 +1,7 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/status" - - "github.com/openkcm/krypton/cli/output" - "github.com/openkcm/krypton/pkg/api/v1/proto/admin" ) func getCmd() *cobra.Command { @@ -21,93 +12,8 @@ func getCmd() *cobra.Command { cmd.AddCommand(getTenantCmd()) cmd.AddCommand(getTenantsCmd()) - - return cmd -} - -func getTenantCmd() *cobra.Command { - var asJSON bool - - cmd := &cobra.Command{ - Use: "tenant ", - Short: "Get a tenant by ID", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // TODO: insecure.NewCredentials is a temporary workaround until TLS is configured - conn, err := grpc.NewClient( - serverAddr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return fmt.Errorf("failed to connect: %w", err) - } - defer conn.Close() - - client := admin.NewTenantServiceClient(conn) - - resp, err := client.GetTenant(cmd.Context(), &admin.GetTenantRequest{ - Id: args[0], - }) - if err != nil { - if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound { - return fmt.Errorf("tenant %q not found", args[0]) - } - return fmt.Errorf("failed to get tenant: %w", err) - } - - tenant := admin.TenantFromProto(resp.GetTenant()) - - builder, err := output.From(tenant) - if err != nil { - return fmt.Errorf("failed to format output: %w", err) - } - - return formatOutput(builder, asJSON).To(cmd.OutOrStdout()) - }, - } - - cmd.Flags().BoolVar(&asJSON, "json", false, "output in JSON format") - - return cmd -} - -func getTenantsCmd() *cobra.Command { - var asJSON bool - - cmd := &cobra.Command{ - Use: "tenants", - Short: "Get tenants", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - // TODO: insecure.NewCredentials is a temporary workaround until TLS is configured - conn, err := grpc.NewClient( - serverAddr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return fmt.Errorf("failed to connect: %w", err) - } - defer conn.Close() - - client := admin.NewTenantServiceClient(conn) - - resp, err := client.ListTenants(cmd.Context(), &admin.ListTenantsRequest{}) - if err != nil { - return fmt.Errorf("failed to list tenants: %w", err) - } - - tenants := admin.TenantsFromProto(resp.GetTenants()) - - builder, err := output.From(tenants) - if err != nil { - return fmt.Errorf("failed to format output: %w", err) - } - - return formatOutput(builder, asJSON).To(cmd.OutOrStdout()) - }, - } - - cmd.Flags().BoolVar(&asJSON, "json", false, "output in JSON format") + cmd.AddCommand(getKeyParentsCmd()) + cmd.AddCommand(getKeyDescendantsCmd()) return cmd } diff --git a/cli/cmd/key.go b/cli/cmd/key.go new file mode 100644 index 0000000..d0e294e --- /dev/null +++ b/cli/cmd/key.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + + "github.com/openkcm/krypton/cli/output" + "github.com/openkcm/krypton/pkg/api/v1/proto/admin" +) + +type keyTreeRow struct { + Kind string + ID string + ParentID string + Name string + ManagedBy string + Status string +} + +func newKeyTreeRow(k *admin.Key) keyTreeRow { + return keyTreeRow{ + Kind: k.GetKind(), + ID: k.GetId(), + ParentID: k.GetParentId(), + Name: k.GetName(), + ManagedBy: k.GetManagedBy(), + Status: k.GetKeyProcessingState().GetStatus(), + } +} + +func getKeyParentsCmd() *cobra.Command { + var keyID, tenantID string + var asJSON bool + + cmd := &cobra.Command{ + Use: "parent-keys", + Short: "Get parent keys by tenant & key ID", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: insecure.NewCredentials is a temporary workaround until TLS is configured + conn, err := grpc.NewClient( + serverAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + defer conn.Close() + + client := admin.NewKeyServiceClient(conn) + + resp, err := client.GetParentKeys(cmd.Context(), &admin.GetParentKeysRequest{ + Id: keyID, + TenantId: tenantID, + }) + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound { + return fmt.Errorf("parent keys for tenant:[%s] and key:[%s] not found", tenantID, keyID) + } + return fmt.Errorf("failed to get parent keys: %w", err) + } + + ks := resp.GetKeys() + treeRows := make([]keyTreeRow, 0, len(ks)) + for _, k := range ks { + treeRows = append(treeRows, newKeyTreeRow(k)) + } + + builder, err := output.From(treeRows) + if err != nil { + return fmt.Errorf("failed to format output: %w", err) + } + + return formatOutput(builder, asJSON).To(cmd.OutOrStdout()) + }, + } + + cmd.Flags().StringVar(&keyID, "key-id", "", "id of the key") + cmd.Flags().StringVar(&tenantID, "tenant-id", "", "id of the tenant") + cmd.Flags().BoolVar(&asJSON, "json", false, "output in JSON format") + _ = cmd.MarkFlagRequired("key-id") + _ = cmd.MarkFlagRequired("tenant-id") + + return cmd +} + +func getKeyDescendantsCmd() *cobra.Command { + var keyID, tenantID string + var asJSON bool + + cmd := &cobra.Command{ + Use: "descendant-keys", + Short: "Get descendants by key & tenant ID", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: insecure.NewCredentials is a temporary workaround until TLS is configured + conn, err := grpc.NewClient( + serverAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + defer conn.Close() + + client := admin.NewKeyServiceClient(conn) + + resp, err := client.GetDescendantKeys(cmd.Context(), &admin.GetDescendantKeysRequest{ + Id: keyID, + TenantId: tenantID, + }) + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound { + return fmt.Errorf("descendant keys for tenant:[%s] and key:[%s] not found", tenantID, keyID) + } + return fmt.Errorf("failed to get descendant keys: %w", err) + } + + totalLen := 0 + trees := resp.GetKeyTree() + for i := range trees { + totalLen += len(trees[i].GetKeys()) + 1 + } + + treeRows := make([]keyTreeRow, 0, totalLen) + for _, tree := range trees { + for _, k := range tree.GetKeys() { + treeRows = append(treeRows, newKeyTreeRow(k)) + } + treeRows = append(treeRows, keyTreeRow{}) // add empty row for space between layers + } + + builder, err := output.From(treeRows) + if err != nil { + return fmt.Errorf("failed to format output: %w", err) + } + + return formatOutput(builder, asJSON).To(cmd.OutOrStdout()) + }, + } + + cmd.Flags().StringVar(&keyID, "key-id", "", "id of the key") + cmd.Flags().StringVar(&tenantID, "tenant-id", "", "id of the tenant") + cmd.Flags().BoolVar(&asJSON, "json", false, "output in JSON format") + _ = cmd.MarkFlagRequired("key-id") + _ = cmd.MarkFlagRequired("tenant-id") + + return cmd +} diff --git a/cli/cmd/tenant.go b/cli/cmd/tenant.go new file mode 100644 index 0000000..5eb3363 --- /dev/null +++ b/cli/cmd/tenant.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + + "github.com/openkcm/krypton/cli/output" + "github.com/openkcm/krypton/pkg/api/v1/proto/admin" +) + +func getTenantCmd() *cobra.Command { + var asJSON bool + + cmd := &cobra.Command{ + Use: "tenant ", + Short: "Get a tenant by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: insecure.NewCredentials is a temporary workaround until TLS is configured + conn, err := grpc.NewClient( + serverAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + defer conn.Close() + + client := admin.NewTenantServiceClient(conn) + + resp, err := client.GetTenant(cmd.Context(), &admin.GetTenantRequest{ + Id: args[0], + }) + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound { + return fmt.Errorf("tenant %q not found", args[0]) + } + return fmt.Errorf("failed to get tenant: %w", err) + } + + tenant := admin.TenantFromProto(resp.GetTenant()) + + builder, err := output.From(tenant) + if err != nil { + return fmt.Errorf("failed to format output: %w", err) + } + + return formatOutput(builder, asJSON).To(cmd.OutOrStdout()) + }, + } + + cmd.Flags().BoolVar(&asJSON, "json", false, "output in JSON format") + + return cmd +} + +func getTenantsCmd() *cobra.Command { + var asJSON bool + + cmd := &cobra.Command{ + Use: "tenants", + Short: "Get tenants", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: insecure.NewCredentials is a temporary workaround until TLS is configured + conn, err := grpc.NewClient( + serverAddr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + defer conn.Close() + + client := admin.NewTenantServiceClient(conn) + + resp, err := client.ListTenants(cmd.Context(), &admin.ListTenantsRequest{}) + if err != nil { + return fmt.Errorf("failed to list tenants: %w", err) + } + + tenants := admin.TenantsFromProto(resp.GetTenants()) + + builder, err := output.From(tenants) + if err != nil { + return fmt.Errorf("failed to format output: %w", err) + } + + return formatOutput(builder, asJSON).To(cmd.OutOrStdout()) + }, + } + + cmd.Flags().BoolVar(&asJSON, "json", false, "output in JSON format") + + return cmd +} diff --git a/integration/key_test.go b/integration/key_test.go new file mode 100644 index 0000000..e08ae73 --- /dev/null +++ b/integration/key_test.go @@ -0,0 +1,290 @@ +package integration + +import ( + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + "github.com/openkcm/krypton/pkg/api/v1/proto/admin" + "github.com/openkcm/krypton/pkg/model" + "github.com/openkcm/krypton/pkg/store" +) + +func TestGetKeys(t *testing.T) { + // given + testDB, _ := createDatabase(t) + tenantStore := newTenantStore(t, testDB) + keyStore := newKeyStore(t, testDB) + + serverAddr := startGRPCServer(t, func(srv *grpc.Server) { + admin.RegisterTenantServiceServer(srv, admin.NewTenantService(tenantStore)) + admin.RegisterKeyServiceServer(srv, admin.NewKeyService(keyStore, tenantStore, defaultTestHierarchy(), &noopJobPreparer{})) + }) + + // create tenant + // `kr create tenant --name --json --server ` + expName := "tenant-" + uuid.NewString() + cmd := newCLICommand(t.Context(), t.TempDir(), "create", "tenant", "--name", expName, "--json", "--server", serverAddr) + output, err := cmd.CombinedOutput() + assert.NoError(t, err, "command should succeed, output: %s", string(output)) + + tenants := decode(t, output) + if !assert.Len(t, tenants, 1) { + return + } + tenantID := tenants[0].ID + + // create hierarchy + hierarchy := createKeyHierarchy(t, keyStore, tenantID) + + t.Run("parent-keys", func(t *testing.T) { + t.Run("should get all parent keys", func(t *testing.T) { + // when + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "parent-keys", "--key-id", hierarchy.e.ID, "--tenant-id", tenantID, "--json", "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.NoError(t, err, "command should succeed, output: %s", string(output)) + + actRes := decodeKeyTreeRow(t, output) + assert.Len(t, actRes, 3) + assert.Equal(t, "A", actRes[0].Name) + assert.Equal(t, hierarchy.root.Kind, actRes[0].Kind) + + assert.Equal(t, "B", actRes[1].Name) + assert.Equal(t, hierarchy.b.Kind, actRes[1].Kind) + + assert.Equal(t, "E", actRes[2].Name) + assert.Equal(t, hierarchy.e.Kind, actRes[2].Kind) + }) + + t.Run("should fail if no parent keys exist", func(t *testing.T) { + // when + unknownID := uuid.NewString() + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "parent-keys", "--key-id", unknownID, "--tenant-id", tenantID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "not found") + }) + + t.Run("should fail if tenant does not exist", func(t *testing.T) { + // when + unknownTenantID := uuid.NewString() + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "parent-keys", "--key-id", hierarchy.e.ID, "--tenant-id", unknownTenantID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "not found") + }) + + t.Run("should fail if key id parameter is not provided", func(t *testing.T) { + // when + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "parent-keys", "--tenant-id", tenantID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "required flag(s) \"key-id\" not set") + }) + + t.Run("should fail if tenant id parameter is not provided", func(t *testing.T) { + // when + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "parent-keys", "--key-id", hierarchy.e.ID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "required flag(s) \"tenant-id\" not set") + }) + }) + + t.Run("descendant-keys", func(t *testing.T) { + t.Run("should get all descendant keys", func(t *testing.T) { + // when + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "descendant-keys", "--key-id", hierarchy.root.ID, "--tenant-id", tenantID, "--json", "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.NoError(t, err, "command should succeed, output: %s", string(output)) + + actRes := decodeKeyTreeRow(t, output) + assert.Len(t, actRes, 12) + + assert.Equal(t, "A", actRes[0].Name) + assert.Equal(t, hierarchy.root.Kind, actRes[0].Kind) + + assert.Empty(t, actRes[1].Name) + + assert.Equal(t, "B", actRes[2].Name) + assert.Equal(t, hierarchy.b.Kind, actRes[2].Kind) + assert.Equal(t, "C", actRes[3].Name) + assert.Equal(t, hierarchy.c.Kind, actRes[3].Kind) + + assert.Empty(t, actRes[4].Name) + + assert.Equal(t, "D", actRes[5].Name) + assert.Equal(t, hierarchy.d.Kind, actRes[5].Kind) + assert.Equal(t, "E", actRes[6].Name) + assert.Equal(t, hierarchy.e.Kind, actRes[6].Kind) + assert.Equal(t, "F", actRes[7].Name) + assert.Equal(t, hierarchy.f.Kind, actRes[7].Kind) + assert.Equal(t, "G", actRes[8].Name) + assert.Equal(t, hierarchy.g.Kind, actRes[8].Kind) + + assert.Empty(t, actRes[9].Name) + + assert.Equal(t, "H", actRes[10].Name) + assert.Equal(t, hierarchy.h.Kind, actRes[10].Kind) + + assert.Empty(t, actRes[11].Name) + }) + + t.Run("should fail if no descendant keys exist", func(t *testing.T) { + // when + unknownID := uuid.NewString() + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "descendant-keys", "--key-id", unknownID, "--tenant-id", tenantID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "not found") + }) + + t.Run("should fail if tenant does not exist", func(t *testing.T) { + // when + unknownTenantID := uuid.NewString() + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "descendant-keys", "--key-id", hierarchy.e.ID, "--tenant-id", unknownTenantID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "not found") + }) + + t.Run("should fail if key id parameter is not provided", func(t *testing.T) { + // when + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "descendant-keys", "--tenant-id", tenantID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "required flag(s) \"key-id\" not set") + }) + + t.Run("should fail if tenant id parameter is not provided", func(t *testing.T) { + // when + cmd = newCLICommand(t.Context(), t.TempDir(), "get", "descendant-keys", "--key-id", hierarchy.e.ID, "--server", serverAddr) + output, err = cmd.CombinedOutput() + + // then + assert.Error(t, err) + assert.Contains(t, string(output), "required flag(s) \"tenant-id\" not set") + }) + }) +} + +type keyTreeRow struct { + Kind model.KeyKind + ID string + ParentID string + Name string + ManagedBy string + Status string +} + +// keyHierarchy holds a test key tree with the following structure: +// +// A(K0) +// B(K1) +// D(K2) +// E(K2) +// C(K1) +// F(K2) +// G(K2) +// H(K3) +type keyHierarchy struct { + root model.Key // A + b model.Key + c model.Key + d model.Key + e model.Key + f model.Key + g model.Key + h model.Key +} + +// createKeyHierarchy sets up a test key hierarchy with 8 keys across 4 levels and returns the created keys for reference in tests. +// keyHierarchy holds a test key tree with the following structure: +// +// A(K0) +// B(K1) +// D(K2) +// E(K2) +// C(K1) +// F(K2) +// G(K2) +// H(K3) +func createKeyHierarchy(t *testing.T, keyStore store.Key, tenantID string) keyHierarchy { + t.Helper() + ctx := t.Context() + + root := model.NewKey(tenantID, "A", "K0", nil, "root", nil) + err := keyStore.CreateKey(ctx, root) + require.NoError(t, err) + + b := model.NewKey(tenantID, "B", "K1", &root.ID, "root", nil) + err = keyStore.CreateKey(ctx, b) + require.NoError(t, err) + + c := model.NewKey(tenantID, "C", "K1", &root.ID, "root", nil) + err = keyStore.CreateKey(ctx, c) + require.NoError(t, err) + + d := model.NewKey(tenantID, "D", "K2", &b.ID, "agent-aws", nil) + err = keyStore.CreateKey(ctx, d) + require.NoError(t, err) + + e := model.NewKey(tenantID, "E", "K2", &b.ID, "agent-azure", nil) + err = keyStore.CreateKey(ctx, e) + require.NoError(t, err) + + f := model.NewKey(tenantID, "F", "K2", &c.ID, "agent-gcp", nil) + err = keyStore.CreateKey(ctx, f) + require.NoError(t, err) + + g := model.NewKey(tenantID, "G", "K2", &c.ID, "agent-onprem", nil) + err = keyStore.CreateKey(ctx, g) + require.NoError(t, err) + + h := model.NewKey(tenantID, "H", "K3", &g.ID, "agent-onprem-2", nil) + err = keyStore.CreateKey(ctx, h) + require.NoError(t, err) + + return keyHierarchy{ + root: root, + b: b, + c: c, + d: d, + e: e, + f: f, + g: g, + h: h, + } +} + +func decodeKeyTreeRow(t *testing.T, output []byte) []keyTreeRow { + t.Helper() + var ts []keyTreeRow + err := json.Unmarshal(output, &ts) + if err != nil { + assert.FailNowf(t, "failed to decode response", "output: %s, error: %v", string(output), err) + } + return ts +} diff --git a/integration/registration_test.go b/integration/registration_test.go index 6ea7946..b199187 100644 --- a/integration/registration_test.go +++ b/integration/registration_test.go @@ -30,7 +30,6 @@ func TestRegistration(t *testing.T) { db, dbConnStr := createDatabase(t) // Create agent store - require.NoError(t, sql.Migrate(ctx, db)) agentStore := sql.NewAgentStore(db) // Build binaries for root server and agent diff --git a/integration/setup_test.go b/integration/setup_test.go index 95942a9..3eb61a4 100644 --- a/integration/setup_test.go +++ b/integration/setup_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/openkcm/orbital" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/modules/postgres" @@ -19,6 +20,8 @@ import ( _ "github.com/lib/pq" + "github.com/openkcm/krypton/internal/cryptor" + "github.com/openkcm/krypton/internal/spec" "github.com/openkcm/krypton/pkg/store" storesql "github.com/openkcm/krypton/pkg/store/sql" ) @@ -120,15 +123,22 @@ func newCLICommand(ctx context.Context, homeDir string, args ...string) *exec.Cm return cmd } -// newTestStore creates a new isolated database and store for testing. -// The database is automatically dropped when the test completes. -func newTestStore(t *testing.T) store.Tenant { +// newTenantStore creates a tenant store +func newTenantStore(t *testing.T, db *sql.DB) store.Tenant { t.Helper() - ctx := t.Context() + if db == nil { + db, _ = createDatabase(t) + } + return storesql.NewTenantStore(db) +} - testDB, _ := createDatabase(t) - require.NoError(t, storesql.Migrate(ctx, testDB)) - return storesql.NewTenantStore(testDB) +// newKeyStore creates a key store. +func newKeyStore(t *testing.T, db *sql.DB) store.Key { + t.Helper() + if db == nil { + db, _ = createDatabase(t) + } + return storesql.NewKeyStore(db) } // createDatabase creates a new PostgreSQL database for testing and returns a connection to it. @@ -165,6 +175,10 @@ func createDatabase(t *testing.T) (*sql.DB, string) { db.Close() } }) + + // migrate + require.NoError(t, storesql.Migrate(ctx, sqlDB)) + return sqlDB, pgConStr } @@ -174,7 +188,7 @@ type RegisterFunc func(*grpc.Server) // startGRPCServer starts a gRPC server and returns the server address. // The register function is called to register services to the server. // The server is automatically stopped when the test completes. -func startGRPCServer(t *testing.T, register RegisterFunc) string { +func startGRPCServer(t *testing.T, registerFns ...RegisterFunc) string { t.Helper() lis, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "localhost:0") @@ -183,7 +197,9 @@ func startGRPCServer(t *testing.T, register RegisterFunc) string { } srv := grpc.NewServer() - register(srv) + for _, registerFn := range registerFns { + registerFn(srv) + } go func() { if err := srv.Serve(lis); err != nil { @@ -197,3 +213,26 @@ func startGRPCServer(t *testing.T, register RegisterFunc) string { return lis.Addr().String() } + +// defaultTestHierarchy mirrors the K0(root) → K1(kek) → K2(tek) → K3(dek) +// hierarchy used by the existing fixture builders below. +func defaultTestHierarchy() spec.KeyHierarchy { + return spec.KeyHierarchy{ + Name: "test-hierarchy", + KeySpecs: []spec.KeySpec{ + {Kind: "K0", Role: spec.KeyRoleRoot, Algorithm: cryptor.KeyAlgorithmAES256}, + {Kind: "K1", Role: spec.KeyRoleKek, Algorithm: cryptor.KeyAlgorithmAES256}, + {Kind: "K2", Role: spec.KeyRoleTek, Algorithm: cryptor.KeyAlgorithmAES256}, + {Kind: "K3", Role: spec.KeyRoleDek, Algorithm: cryptor.KeyAlgorithmAES256}, + }, + } +} + +type noopJobPreparer struct{} + +func (*noopJobPreparer) PrepareJob(_ context.Context, job orbital.Job) (orbital.Job, error) { + if job.ID == uuid.Nil { + job.ID = uuid.Must(uuid.NewUUID()) + } + return job, nil +} diff --git a/integration/tenant_test.go b/integration/tenant_test.go index 9fd5f56..d6bc46a 100644 --- a/integration/tenant_test.go +++ b/integration/tenant_test.go @@ -25,7 +25,7 @@ type expTenant struct { } func TestCreateTenant(t *testing.T) { - tenantStore := newTestStore(t) + tenantStore := newTenantStore(t, nil) serverAddr := startGRPCServer(t, func(srv *grpc.Server) { admin.RegisterTenantServiceServer(srv, admin.NewTenantService(tenantStore)) }) @@ -103,7 +103,7 @@ func TestCreateTenant(t *testing.T) { } func TestGetTenant(t *testing.T) { - tenantStore := newTestStore(t) + tenantStore := newTenantStore(t, nil) serverAddr := startGRPCServer(t, func(srv *grpc.Server) { admin.RegisterTenantServiceServer(srv, admin.NewTenantService(tenantStore)) }) @@ -163,7 +163,7 @@ func TestGetTenant(t *testing.T) { func TestListTenants(t *testing.T) { t.Run("returns empty list when no tenants exist", func(t *testing.T) { // given - tenantStore := newTestStore(t) + tenantStore := newTenantStore(t, nil) serverAddr := startGRPCServer(t, func(srv *grpc.Server) { admin.RegisterTenantServiceServer(srv, admin.NewTenantService(tenantStore)) }) @@ -180,7 +180,7 @@ func TestListTenants(t *testing.T) { t.Run("lists created tenants", func(t *testing.T) { // given - tenantStore := newTestStore(t) + tenantStore := newTenantStore(t, nil) serverAddr := startGRPCServer(t, func(srv *grpc.Server) { admin.RegisterTenantServiceServer(srv, admin.NewTenantService(tenantStore)) }) @@ -223,7 +223,7 @@ func TestListTenants(t *testing.T) { // Integration tests use exec.Command which provides piped stdin, not a real TTY. // Interactive selection is covered by unit tests in cli/output/terminal/. func TestSelectTenant(t *testing.T) { - tenantStore := newTestStore(t) + tenantStore := newTenantStore(t, nil) serverAddr := startGRPCServer(t, func(srv *grpc.Server) { admin.RegisterTenantServiceServer(srv, admin.NewTenantService(tenantStore)) }) diff --git a/pkg/api/v1/proto/admin/key_convert.go b/pkg/api/v1/proto/admin/key_convert.go index 9d86d11..f1b76a6 100644 --- a/pkg/api/v1/proto/admin/key_convert.go +++ b/pkg/api/v1/proto/admin/key_convert.go @@ -28,7 +28,7 @@ func KeyToProto(k model.Key) *Key { } } -func KeyTreeToProto(tree model.KeyTreeTraverser) []*KeyTree { +func KeyTreeTraverserToProto(tree model.KeyTreeTraverser) []*KeyTree { var res []*KeyTree for layer := range tree.IterKeysByLayerAsc() { res = append(res, &KeyTree{Keys: KeysToProto(layer)}) diff --git a/pkg/api/v1/proto/admin/key_service.go b/pkg/api/v1/proto/admin/key_service.go index c3a09df..93dcacb 100644 --- a/pkg/api/v1/proto/admin/key_service.go +++ b/pkg/api/v1/proto/admin/key_service.go @@ -341,6 +341,6 @@ func (s *KeyService) GetDescendantKeys(ctx context.Context, req *GetDescendantKe } return &GetDescendantKeysResponse{ - KeyTree: KeyTreeToProto(res.KeyTree), + KeyTree: KeyTreeTraverserToProto(res.KeyTree), }, nil } From da1c97d242cdbeb5aab4de027ffe187571efc8bd Mon Sep 17 00:00:00 2001 From: jK <33685667+jithinkunjachan@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:39:12 +0200 Subject: [PATCH 2/2] fix: remove additional json rows Signed-off-by: jK <33685667+jithinkunjachan@users.noreply.github.com> --- cli/cmd/key.go | 9 +++++++-- integration/key_test.go | 42 +++++++++++++++++------------------------ 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/cli/cmd/key.go b/cli/cmd/key.go index d0e294e..92f5ffa 100644 --- a/cli/cmd/key.go +++ b/cli/cmd/key.go @@ -124,7 +124,10 @@ func getKeyDescendantsCmd() *cobra.Command { totalLen := 0 trees := resp.GetKeyTree() for i := range trees { - totalLen += len(trees[i].GetKeys()) + 1 + totalLen += len(trees[i].GetKeys()) + if !asJSON { + totalLen++ + } } treeRows := make([]keyTreeRow, 0, totalLen) @@ -132,7 +135,9 @@ func getKeyDescendantsCmd() *cobra.Command { for _, k := range tree.GetKeys() { treeRows = append(treeRows, newKeyTreeRow(k)) } - treeRows = append(treeRows, keyTreeRow{}) // add empty row for space between layers + if !asJSON { + treeRows = append(treeRows, keyTreeRow{}) // add empty row for space between layers + } } builder, err := output.From(treeRows) diff --git a/integration/key_test.go b/integration/key_test.go index e08ae73..c7d0cfb 100644 --- a/integration/key_test.go +++ b/integration/key_test.go @@ -115,35 +115,27 @@ func TestGetKeys(t *testing.T) { assert.NoError(t, err, "command should succeed, output: %s", string(output)) actRes := decodeKeyTreeRow(t, output) - assert.Len(t, actRes, 12) + assert.Len(t, actRes, 8) assert.Equal(t, "A", actRes[0].Name) assert.Equal(t, hierarchy.root.Kind, actRes[0].Kind) - assert.Empty(t, actRes[1].Name) - - assert.Equal(t, "B", actRes[2].Name) - assert.Equal(t, hierarchy.b.Kind, actRes[2].Kind) - assert.Equal(t, "C", actRes[3].Name) - assert.Equal(t, hierarchy.c.Kind, actRes[3].Kind) - - assert.Empty(t, actRes[4].Name) - - assert.Equal(t, "D", actRes[5].Name) - assert.Equal(t, hierarchy.d.Kind, actRes[5].Kind) - assert.Equal(t, "E", actRes[6].Name) - assert.Equal(t, hierarchy.e.Kind, actRes[6].Kind) - assert.Equal(t, "F", actRes[7].Name) - assert.Equal(t, hierarchy.f.Kind, actRes[7].Kind) - assert.Equal(t, "G", actRes[8].Name) - assert.Equal(t, hierarchy.g.Kind, actRes[8].Kind) - - assert.Empty(t, actRes[9].Name) - - assert.Equal(t, "H", actRes[10].Name) - assert.Equal(t, hierarchy.h.Kind, actRes[10].Kind) - - assert.Empty(t, actRes[11].Name) + assert.Equal(t, "B", actRes[1].Name) + assert.Equal(t, hierarchy.b.Kind, actRes[1].Kind) + assert.Equal(t, "C", actRes[2].Name) + assert.Equal(t, hierarchy.c.Kind, actRes[2].Kind) + + assert.Equal(t, "D", actRes[3].Name) + assert.Equal(t, hierarchy.d.Kind, actRes[3].Kind) + assert.Equal(t, "E", actRes[4].Name) + assert.Equal(t, hierarchy.e.Kind, actRes[4].Kind) + assert.Equal(t, "F", actRes[5].Name) + assert.Equal(t, hierarchy.f.Kind, actRes[5].Kind) + assert.Equal(t, "G", actRes[6].Name) + assert.Equal(t, hierarchy.g.Kind, actRes[6].Kind) + + assert.Equal(t, "H", actRes[7].Name) + assert.Equal(t, hierarchy.h.Kind, actRes[7].Kind) }) t.Run("should fail if no descendant keys exist", func(t *testing.T) {