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
64 changes: 48 additions & 16 deletions pkg/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
"strings"
"time"

secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets"
"github.com/windsorcli/cli/pkg/runtime/config"
"github.com/windsorcli/cli/pkg/runtime/env"
"github.com/windsorcli/cli/pkg/runtime/secrets"
secretsRuntime "github.com/windsorcli/cli/pkg/runtime/secrets"
"github.com/windsorcli/cli/pkg/runtime/shell"
"github.com/windsorcli/cli/pkg/runtime/tools"
)
Expand Down Expand Up @@ -42,8 +43,8 @@ type Runtime struct {

// SecretsProviders contains providers for Sops and 1Password secrets management
SecretsProviders struct {
Sops secrets.SecretsProvider
Onepassword secrets.SecretsProvider
Sops secretsRuntime.SecretsProvider
Onepassword []secretsRuntime.SecretsProvider
}

// EnvPrinters contains environment printers for various providers and tools
Expand Down Expand Up @@ -219,9 +220,9 @@ func (rt *Runtime) LoadEnvironment(decrypt bool) error {
return fmt.Errorf("config handler not loaded")
}

rt.initializeSecretsProviders()
rt.initializeEnvPrinters()
rt.initializeToolsManager()
rt.initializeSecretsProviders()

if err := rt.initializeComponents(); err != nil {
return fmt.Errorf("failed to initialize environment components: %w", err)
Expand Down Expand Up @@ -405,12 +406,12 @@ func (rt *Runtime) initializeEnvPrinters() {
rt.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(rt.Shell, rt.ConfigHandler)
}
if rt.EnvPrinters.WindsorEnv == nil {
secretsProviders := []secrets.SecretsProvider{}
secretsProviders := []secretsRuntime.SecretsProvider{}
if rt.SecretsProviders.Sops != nil {
secretsProviders = append(secretsProviders, rt.SecretsProviders.Sops)
}
if rt.SecretsProviders.Onepassword != nil {
secretsProviders = append(secretsProviders, rt.SecretsProviders.Onepassword)
secretsProviders = append(secretsProviders, rt.SecretsProviders.Onepassword...)
}
allEnvPrinters := rt.getAllEnvPrinters()
rt.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(rt.Shell, rt.ConfigHandler, secretsProviders, allEnvPrinters)
Expand All @@ -429,16 +430,44 @@ func (rt *Runtime) initializeToolsManager() {

// initializeSecretsProviders initializes secrets providers based on current configuration settings.
// The method sets up the SOPS provider if enabled with the context's config root path, and sets up
// the 1Password provider if enabled, using a mock in test scenarios. Providers are only initialized
// if not already present on the context.
// 1Password providers for each configured vault. Providers are only initialized if not already present.
func (rt *Runtime) initializeSecretsProviders() {
if rt.SecretsProviders.Sops == nil && rt.ConfigHandler.GetBool("secrets.sops.enabled", false) {
configPath := rt.ConfigRoot
rt.SecretsProviders.Sops = secrets.NewSopsSecretsProvider(configPath, rt.Shell)
}

if rt.SecretsProviders.Onepassword == nil && rt.ConfigHandler.GetBool("secrets.onepassword.enabled", false) {
rt.SecretsProviders.Onepassword = secrets.NewMockSecretsProvider(rt.Shell)
rt.SecretsProviders.Sops = secretsRuntime.NewSopsSecretsProvider(configPath, rt.Shell)
}

if rt.SecretsProviders.Onepassword == nil {
vaultsValue := rt.ConfigHandler.Get("secrets.onepassword.vaults")
if vaultsValue != nil {
if vaultsMap, ok := vaultsValue.(map[string]any); ok {
rt.SecretsProviders.Onepassword = []secretsRuntime.SecretsProvider{}
for vaultID, vaultData := range vaultsMap {
if vaultMap, ok := vaultData.(map[string]any); ok {
vault := secretsConfigType.OnePasswordVault{
ID: vaultID,
}
if url, ok := vaultMap["url"].(string); ok {
vault.URL = url
}
if name, ok := vaultMap["name"].(string); ok {
vault.Name = name
}
if id, ok := vaultMap["id"].(string); ok && id != "" {
vault.ID = id
}

var provider secretsRuntime.SecretsProvider
if os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" {
provider = secretsRuntime.NewOnePasswordSDKSecretsProvider(vault, rt.Shell)
} else {
provider = secretsRuntime.NewOnePasswordCLISecretsProvider(vault, rt.Shell)
}
rt.SecretsProviders.Onepassword = append(rt.SecretsProviders.Onepassword, provider)
}
}
}
}
}
}

Expand Down Expand Up @@ -467,9 +496,12 @@ func (rt *Runtime) initializeComponents() error {
// loadSecrets loads secrets from configured secrets providers.
// It attempts to load secrets from both SOPS and 1Password providers if they are available.
func (rt *Runtime) loadSecrets() error {
providers := []secrets.SecretsProvider{
rt.SecretsProviders.Sops,
rt.SecretsProviders.Onepassword,
providers := []secretsRuntime.SecretsProvider{}
if rt.SecretsProviders.Sops != nil {
providers = append(providers, rt.SecretsProviders.Sops)
}
if rt.SecretsProviders.Onepassword != nil {
providers = append(providers, rt.SecretsProviders.Onepassword...)
}

for _, provider := range providers {
Expand Down
184 changes: 170 additions & 14 deletions pkg/runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ func TestRuntime_NewRuntime(t *testing.T) {
},
}
rtOpts[0].SecretsProviders.Sops = mockSopsProvider
rtOpts[0].SecretsProviders.Onepassword = mockOnepasswordProvider
rtOpts[0].SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider}
rtOpts[0].EnvPrinters.AwsEnv = mockAwsEnv
rtOpts[0].EnvPrinters.AzureEnv = mockAzureEnv
rtOpts[0].EnvPrinters.DockerEnv = mockDockerEnv
Expand Down Expand Up @@ -436,7 +436,7 @@ func TestRuntime_NewRuntime(t *testing.T) {
t.Error("Expected Sops provider to be set")
}

if rt.SecretsProviders.Onepassword != mockOnepasswordProvider {
if len(rt.SecretsProviders.Onepassword) != 1 || rt.SecretsProviders.Onepassword[0] != mockOnepasswordProvider {
t.Error("Expected Onepassword provider to be set")
}

Expand Down Expand Up @@ -625,7 +625,7 @@ func TestRuntime_LoadEnvironment(t *testing.T) {
mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell)

rt.SecretsProviders.Sops = mockSopsProvider
rt.SecretsProviders.Onepassword = mockOnepasswordProvider
rt.SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider}

// When LoadEnvironment is called with secrets enabled
err := rt.LoadEnvironment(true)
Expand Down Expand Up @@ -2306,7 +2306,7 @@ func TestRuntime_loadSecrets(t *testing.T) {
mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell)

rt.SecretsProviders.Sops = mockSopsProvider
rt.SecretsProviders.Onepassword = mockOnepasswordProvider
rt.SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider}

// When loadSecrets is called
err := rt.loadSecrets()
Expand Down Expand Up @@ -2402,27 +2402,35 @@ func TestRuntime_initializeSecretsProviders(t *testing.T) {
}
})

t.Run("InitializesOnepasswordProviderWhenEnabled", func(t *testing.T) {
// Given a runtime with 1Password enabled in config
t.Run("InitializesOnepasswordProviderWhenVaultsConfigured", func(t *testing.T) {
// Given a runtime with 1Password vaults configured
mocks := setupRuntimeMocks(t)
rt := mocks.Runtime

mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool {
if key == "secrets.onepassword.enabled" {
return true
mockConfigHandler.GetFunc = func(key string) any {
if key == "secrets.onepassword.vaults" {
return map[string]any{
"personal": map[string]any{
"url": "my.1password.com",
"name": "Personal",
},
}
}
return false
return nil
}

// When initializeSecretsProviders is called
rt.initializeSecretsProviders()

// Then 1Password provider should be initialized
// Then 1Password provider should be initialized for each vault

if rt.SecretsProviders.Onepassword == nil {
if len(rt.SecretsProviders.Onepassword) == 0 {
t.Error("Expected 1Password provider to be initialized")
}
if len(rt.SecretsProviders.Onepassword) != 1 {
t.Errorf("Expected 1 provider, got %d", len(rt.SecretsProviders.Onepassword))
}
})

t.Run("SkipsProvidersWhenDisabled", func(t *testing.T) {
Expand All @@ -2444,7 +2452,7 @@ func TestRuntime_initializeSecretsProviders(t *testing.T) {
t.Error("Expected SOPS provider to be nil when disabled")
}

if rt.SecretsProviders.Onepassword != nil {
if len(rt.SecretsProviders.Onepassword) > 0 {
t.Error("Expected 1Password provider to be nil when disabled")
}
})
Expand Down Expand Up @@ -2473,6 +2481,154 @@ func TestRuntime_initializeSecretsProviders(t *testing.T) {
t.Error("Expected existing provider to be preserved")
}
})

t.Run("InitializesMultipleVaults", func(t *testing.T) {
// Given a runtime with multiple 1Password vaults configured
mocks := setupRuntimeMocks(t)
rt := mocks.Runtime

mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetFunc = func(key string) any {
if key == "secrets.onepassword.vaults" {
return map[string]any{
"personal": map[string]any{
"url": "my.1password.com",
"name": "Personal",
},
"work": map[string]any{
"url": "company.1password.com",
"name": "Work",
},
}
}
return nil
}

// When initializeSecretsProviders is called
rt.initializeSecretsProviders()

// Then multiple providers should be initialized
if len(rt.SecretsProviders.Onepassword) == 0 {
t.Error("Expected 1Password providers to be initialized")
}
if len(rt.SecretsProviders.Onepassword) != 2 {
t.Errorf("Expected 2 providers, got %d", len(rt.SecretsProviders.Onepassword))
}
})

t.Run("HandlesEmptyVaultsMap", func(t *testing.T) {
// Given a runtime with empty vaults map
mocks := setupRuntimeMocks(t)
rt := mocks.Runtime

mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetFunc = func(key string) any {
if key == "secrets.onepassword.vaults" {
return map[string]any{}
}
return nil
}

// When initializeSecretsProviders is called
rt.initializeSecretsProviders()

// Then no providers should be initialized
if len(rt.SecretsProviders.Onepassword) > 0 {
t.Error("Expected no providers to be initialized for empty vaults map")
}
})

t.Run("HandlesInvalidVaultData", func(t *testing.T) {
// Given a runtime with invalid vault data structure
mocks := setupRuntimeMocks(t)
rt := mocks.Runtime

mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetFunc = func(key string) any {
if key == "secrets.onepassword.vaults" {
return map[string]any{
"personal": "not-a-map",
}
}
return nil
}

// When initializeSecretsProviders is called
rt.initializeSecretsProviders()

// Then no providers should be initialized for invalid data
if len(rt.SecretsProviders.Onepassword) > 0 {
t.Error("Expected no providers to be initialized for invalid vault data")
}
})

t.Run("HandlesVaultWithExplicitID", func(t *testing.T) {
// Given a runtime with vault that has explicit ID field
mocks := setupRuntimeMocks(t)
rt := mocks.Runtime

mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetFunc = func(key string) any {
if key == "secrets.onepassword.vaults" {
return map[string]any{
"personal": map[string]any{
"id": "custom-vault-id",
"url": "my.1password.com",
"name": "Personal",
},
}
}
return nil
}

// When initializeSecretsProviders is called
rt.initializeSecretsProviders()

// Then provider should be initialized with explicit ID
if len(rt.SecretsProviders.Onepassword) == 0 {
t.Error("Expected 1Password provider to be initialized")
}
if len(rt.SecretsProviders.Onepassword) != 1 {
t.Errorf("Expected 1 provider, got %d", len(rt.SecretsProviders.Onepassword))
}
})

t.Run("InitializesProvidersBeforeWindsorEnv", func(t *testing.T) {
// Given a runtime with vaults configured
mocks := setupRuntimeMocks(t)
rt := mocks.Runtime

mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler)
mockConfigHandler.GetFunc = func(key string) any {
if key == "secrets.onepassword.vaults" {
return map[string]any{
"personal": map[string]any{
"url": "my.1password.com",
"name": "Personal",
},
}
}
return nil
}

// When LoadEnvironment is called
err := rt.LoadEnvironment(false)

// Then no error should occur
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}

// And providers should be initialized
if len(rt.SecretsProviders.Onepassword) == 0 {
t.Error("Expected 1Password providers to be initialized")
}

// And WindsorEnv should be initialized with providers
if rt.EnvPrinters.WindsorEnv == nil {
t.Error("Expected WindsorEnv to be initialized")
}
})
}

func TestRuntime_initializeComponents_EdgeCases(t *testing.T) {
Expand Down Expand Up @@ -2736,7 +2892,7 @@ func TestRuntime_initializeEnvPrinters(t *testing.T) {
mockSopsProvider := secrets.NewMockSecretsProvider(mocks.Shell)
mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell)
rt.SecretsProviders.Sops = mockSopsProvider
rt.SecretsProviders.Onepassword = mockOnepasswordProvider
rt.SecretsProviders.Onepassword = []secrets.SecretsProvider{mockOnepasswordProvider}

// When initializeEnvPrinters is called
rt.initializeEnvPrinters()
Expand Down
Loading