Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions internal/models/topology.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package models

import "errors"

var (
ErrStartKindEmpty = errors.New("start kind cannot be empty")
ErrEndKindEmpty = errors.New("end kind cannot be empty")
ErrVaultConfigMissing = errors.New("vault configuration missing for key binding")
ErrVaultNameEmpty = errors.New("vault name cannot be empty")
ErrVaultTypeEmpty = errors.New("vault type cannot be empty")
ErrParentKeyProviderAgentEmpty = errors.New("parent key provider agent name cannot be empty")
ErrAgentNameEmpty = errors.New("agent name cannot be empty")
ErrKeyBindingsEmpty = errors.New("key bindings cannot be empty")
)

// Labels is a key-value map for metadata
type Labels map[string]string

// HierarchySegment represents a contiguous range of key kinds in the hierarchy
type HierarchySegment struct {
StartKind string // First key kind in segment (e.g., "K2")
EndKind string // Last key kind in segment (e.g., "K3") - inclusive
}

// ParentKeyProviderRef specifies which agent provides parent keys for unwrapping
type ParentKeyProviderRef struct {
AgentName string // Agent name that provides parent keys
}

// VaultSpec holds storage backend configuration
type VaultSpec struct {
Name string // Vault identifier
Type string // Vault type (e.g., "open-bao", "aws-kms", "gcp-kms")
Params map[string]any // Type-specific configuration
}

// KeyBinding encapsulates all dependencies needed to implement a key kind
type KeyBinding struct {
Vault VaultSpec // Storage backend configuration
ParentKeyProvider *ParentKeyProviderRef // Where to get parent keys for unwrapping
Labels Labels // Per-binding labels
}

// TopologySegment defines an agent's portion of the hierarchy
type TopologySegment struct {
Name string // Agent name (must match cert CN)
Segment HierarchySegment // Keys this agent manages
KeyBindings map[string]KeyBinding // All dependencies per key kind (key = kind name)
Labels Labels // Labels assigned to this agent
}

// Topology defines the deployment layout
type Topology struct {
Segments []TopologySegment // List of agent segments (0 to N agents)
}

func (hs *HierarchySegment) Validate() error {
// NOTE: we dont have key kinds defined yet but when we do we should validate that EndKind > StartKind.
// So key kind should implement some sort of comparable interface.
if hs.StartKind == "" {
return ErrStartKindEmpty
}
if hs.EndKind == "" {
return ErrEndKindEmpty
}

return nil
}

func (kb *KeyBinding) Validate() error {
if kb.Vault.Name == "" && kb.Vault.Type == "" {
return ErrVaultConfigMissing
}
if kb.Vault.Name == "" {
return ErrVaultNameEmpty
}
if kb.Vault.Type == "" {
return ErrVaultTypeEmpty
}
if kb.ParentKeyProvider != nil && kb.ParentKeyProvider.AgentName == "" {
return ErrParentKeyProviderAgentEmpty
}

return nil
}

func (ts *TopologySegment) Validate() error {
if ts.Name == "" {
return ErrAgentNameEmpty
}
if err := ts.Segment.Validate(); err != nil {
return err
}
if len(ts.KeyBindings) == 0 {
return ErrKeyBindingsEmpty
}
return nil
}
276 changes: 276 additions & 0 deletions internal/models/topology_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
package models

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestValidateHierarchySegment(t *testing.T) {
tests := []struct {
name string
segment HierarchySegment
wantErr error
}{
{
name: "valid segment",
segment: HierarchySegment{
StartKind: "K2",
EndKind: "K3",
},
wantErr: nil,
},
{
name: "same start and end kind",
segment: HierarchySegment{
StartKind: "K4",
EndKind: "K4",
},
wantErr: nil,
},
{
name: "empty start kind",
segment: HierarchySegment{
StartKind: "",
EndKind: "K3",
},
wantErr: ErrStartKindEmpty,
},
{
name: "empty end kind",
segment: HierarchySegment{
StartKind: "K2",
EndKind: "",
},
wantErr: ErrEndKindEmpty,
},
{
name: "both empty",
segment: HierarchySegment{
StartKind: "",
EndKind: "",
},
wantErr: ErrStartKindEmpty,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.segment.Validate()
if tt.wantErr == nil {
assert.NoError(t, err)
} else {
assert.ErrorIs(t, err, tt.wantErr)
}
})
}
}

func TestValidateKeyBinding(t *testing.T) {
tests := []struct {
name string
binding KeyBinding
wantErr error
}{
{
name: "valid binding with all fields",
binding: KeyBinding{
Vault: VaultSpec{
Name: "my-vault",
Type: "aws-kms",
Params: map[string]any{"region": "us-east-1"},
},
ParentKeyProvider: &ParentKeyProviderRef{
AgentName: "root",
},
Labels: Labels{"env": "prod"},
},
wantErr: nil,
},
{
name: "valid binding without parent key provider",
binding: KeyBinding{
Vault: VaultSpec{
Name: "my-vault",
Type: "open-bao",
},
},
wantErr: nil,
},
{
name: "valid binding with nil params",
binding: KeyBinding{
Vault: VaultSpec{
Name: "my-vault",
Type: "gcp-kms",
},
},
wantErr: nil,
},
{
name: "missing vault configuration",
binding: KeyBinding{
Vault: VaultSpec{},
},
wantErr: ErrVaultConfigMissing,
},
{
name: "empty vault name",
binding: KeyBinding{
Vault: VaultSpec{
Name: "",
Type: "aws-kms",
},
},
wantErr: ErrVaultNameEmpty,
},
{
name: "empty vault type",
binding: KeyBinding{
Vault: VaultSpec{
Name: "my-vault",
Type: "",
},
},
wantErr: ErrVaultTypeEmpty,
},
{
name: "empty parent key provider agent name",
binding: KeyBinding{
Vault: VaultSpec{
Name: "my-vault",
Type: "aws-kms",
},
ParentKeyProvider: &ParentKeyProviderRef{
AgentName: "",
},
},
wantErr: ErrParentKeyProviderAgentEmpty,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.binding.Validate()
if tt.wantErr == nil {
assert.NoError(t, err)
} else {
assert.ErrorIs(t, err, tt.wantErr)
}
})
}
}

func TestValidateTopologySegment(t *testing.T) {
validKeyBindings := map[string]KeyBinding{
"K2": {
Vault: VaultSpec{
Name: "vault-k2",
Type: "aws-kms",
},
},
}

tests := []struct {
name string
segment TopologySegment
wantErr error
}{
{
name: "valid topology segment",
segment: TopologySegment{
Name: "agent-aws",
Segment: HierarchySegment{
StartKind: "K2",
EndKind: "K3",
},
KeyBindings: validKeyBindings,
Labels: Labels{"cloud": "aws"},
},
wantErr: nil,
},
{
name: "valid topology segment without labels",
segment: TopologySegment{
Name: "agent-gcp",
Segment: HierarchySegment{
StartKind: "K2",
EndKind: "K3",
},
KeyBindings: validKeyBindings,
},
wantErr: nil,
},
{
name: "empty agent name",
segment: TopologySegment{
Name: "",
Segment: HierarchySegment{
StartKind: "K2",
EndKind: "K3",
},
KeyBindings: validKeyBindings,
},
wantErr: ErrAgentNameEmpty,
},
{
name: "invalid hierarchy segment - empty start",
segment: TopologySegment{
Name: "agent-aws",
Segment: HierarchySegment{
StartKind: "",
EndKind: "K3",
},
KeyBindings: validKeyBindings,
},
wantErr: ErrStartKindEmpty,
},
{
name: "invalid hierarchy segment - empty end",
segment: TopologySegment{
Name: "agent-aws",
Segment: HierarchySegment{
StartKind: "K2",
EndKind: "",
},
KeyBindings: validKeyBindings,
},
wantErr: ErrEndKindEmpty,
},
{
name: "empty key bindings",
segment: TopologySegment{
Name: "agent-aws",
Segment: HierarchySegment{
StartKind: "K2",
EndKind: "K3",
},
KeyBindings: map[string]KeyBinding{},
},
wantErr: ErrKeyBindingsEmpty,
},
{
name: "nil key bindings",
segment: TopologySegment{
Name: "agent-aws",
Segment: HierarchySegment{
StartKind: "K2",
EndKind: "K3",
},
KeyBindings: nil,
},
wantErr: ErrKeyBindingsEmpty,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.segment.Validate()
if tt.wantErr == nil {
assert.NoError(t, err)
} else {
assert.ErrorIs(t, err, tt.wantErr)
}
})
}
}
Loading