Skip to content
Closed
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
22 changes: 19 additions & 3 deletions pkg/api/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,26 @@ func (s *RegistrarDescriptor) Copy() RegistrarDescriptor {
}

func (s *SecretsConfigDescriptor) Copy() SecretsConfigDescriptor {
var secretsConfigCopy *EnvironmentSecretsConfig
if s.SecretsConfig != nil {
secretsConfigCopy = &EnvironmentSecretsConfig{
Mode: s.SecretsConfig.Mode,
Secrets: make(map[string]SecretsConfigMap),
}
for k, v := range s.SecretsConfig.Secrets {
secretsConfigCopy.Secrets[k] = SecretsConfigMap{
InheritAll: v.InheritAll,
Include: append([]string{}, v.Include...),
Exclude: append([]string{}, v.Exclude...),
Override: lo.Assign(map[string]string{}, v.Override),
}
}
}
return SecretsConfigDescriptor{
Type: s.Type,
Config: s.Config.Copy(),
Inherit: s.Inherit,
Type: s.Type,
Config: s.Config.Copy(),
Inherit: s.Inherit,
SecretsConfig: secretsConfigCopy,
}
}

Expand Down
14 changes: 14 additions & 0 deletions pkg/api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ func (m *StacksMap) ReconcileForDeploy(params StackParams) (*StacksMap, error) {
if parentStack, ok := current[parentStackName]; ok {
stack.Server = parentStack.Server.Copy()
stack.Secrets = parentStack.Secrets.Copy()

// Apply environment-specific secret filtering for child stacks
if stack.Server.Secrets.SecretsConfig != nil {
resolver := NewSecretResolver(stack.Server.Secrets.SecretsConfig)
env := params.Environment
if clientDesc.ParentEnv != "" {
env = clientDesc.ParentEnv
}
filteredSecrets, err := resolver.ResolveSecrets(stack.Secrets.Values, env)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve secrets for stack %q in environment %q", stackName, env)
}
stack.Secrets.Values = filteredSecrets
}
} else {
return nil, errors.Errorf("parent stack %q is not configured for %q in %q", clientDesc.ParentStack, stackName, params.Environment)
}
Expand Down
57 changes: 57 additions & 0 deletions pkg/api/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ func ReadServerConfigs(descriptor *ServerDescriptor) (*ServerDescriptor, error)
res = *withSecrets
}

if withSecretsConfig, err := DetectSecretsConfigType(&res); err != nil {
return nil, err
} else {
res = *withSecretsConfig
}

if withTemplates, err := DetectTemplatesType(&res); err != nil {
return nil, err
} else {
Expand Down Expand Up @@ -302,6 +308,57 @@ func DetectSecretsType(descriptor *ServerDescriptor) (*ServerDescriptor, error)
return descriptor, nil
}

func DetectSecretsConfigType(descriptor *ServerDescriptor) (*ServerDescriptor, error) {
if descriptor.Secrets.IsInherited() {
return descriptor, nil
}
// SecretsConfig is optional, skip if not set
if descriptor.Secrets.SecretsConfig == nil {
return descriptor, nil
}

// Validate the mode
if descriptor.Secrets.SecretsConfig.Mode == "" {
return nil, errors.Errorf("secretsConfig.mode is required when secretsConfig is specified")
}

// Validate mode is one of: include, exclude, override
validModes := map[string]bool{
"include": true,
"exclude": true,
"override": true,
}
if !validModes[descriptor.Secrets.SecretsConfig.Mode] {
return nil, errors.Errorf("invalid secretsConfig.mode %q, must be one of: include, exclude, override", descriptor.Secrets.SecretsConfig.Mode)
}

// Validate each environment config
for envName, envConfig := range descriptor.Secrets.SecretsConfig.Secrets {
// Validate include mode
if descriptor.Secrets.SecretsConfig.Mode == "include" {
if len(envConfig.Include) == 0 {
return nil, errors.Errorf("secretsConfig.secrets.%s: at least one secret must be specified in include mode", envName)
}
}

// Validate exclude mode
if descriptor.Secrets.SecretsConfig.Mode == "exclude" {
if !envConfig.InheritAll && len(envConfig.Exclude) == 0 {
return nil, errors.Errorf("secretsConfig.secrets.%s: inheritAll must be true or exclude must not be empty in exclude mode", envName)
}
}

// Validate override mode
if descriptor.Secrets.SecretsConfig.Mode == "override" {
if len(envConfig.Override) == 0 {
return nil, errors.Errorf("secretsConfig.secrets.%s: at least one override must be specified in override mode", envName)
}
}
}

return descriptor, nil
}

func DetectProvisionerType(descriptor *ServerDescriptor) (*ServerDescriptor, error) {
if descriptor.Provisioner.IsInherited() {
return descriptor, nil
Expand Down
218 changes: 217 additions & 1 deletion pkg/api/secrets.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package api

import "github.com/pkg/errors"
import (
"regexp"
"strings"

"github.com/pkg/errors"
)

const SecretsSchemaVersion = "1.0"

Expand All @@ -24,3 +29,214 @@ func (a *AuthDescriptor) AuthConfig() (AuthConfig, error) {
}
return c, nil
}

// SecretResolver handles environment-specific secret resolution
type SecretResolver struct {
config *EnvironmentSecretsConfig
secretRef *regexp.Regexp
}

// NewSecretResolver creates a new secret resolver
func NewSecretResolver(config *EnvironmentSecretsConfig) *SecretResolver {
return &SecretResolver{
config: config,
secretRef: regexp.MustCompile(`^\$\{secret:([^}]+)\}$`),
}
}

// ResolveSecrets applies environment-specific filtering to secrets
// Returns a filtered map of secret key-value pairs for the specified environment
func (sr *SecretResolver) ResolveSecrets(allSecrets map[string]string, env string) (map[string]string, error) {
if sr.config == nil {
// No filtering configured, return all secrets
return allSecrets, nil
}

envConfig, ok := sr.config.Secrets[env]
if !ok {
// No config for this environment, return all secrets (backwards compatibility)
return allSecrets, nil
}

switch sr.config.Mode {
case "include":
return sr.resolveIncludeMode(allSecrets, envConfig)
case "exclude":
return sr.resolveExcludeMode(allSecrets, envConfig)
case "override":
return sr.resolveOverrideMode(allSecrets, envConfig)
default:
return nil, errors.Errorf("unknown secrets config mode: %q", sr.config.Mode)
}
}

// resolveIncludeMode resolves secrets in include mode
// Only secrets listed in the Include array are available
func (sr *SecretResolver) resolveIncludeMode(allSecrets map[string]string, envConfig SecretsConfigMap) (map[string]string, error) {
result := make(map[string]string)

for _, ref := range envConfig.Include {
value, err := sr.resolveSecretReference(ref, allSecrets)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve secret reference %q", ref)
}
// Extract the key name from the reference
key := sr.extractKeyFromReference(ref)
result[key] = value
}

return result, nil
}

// resolveExcludeMode resolves secrets in exclude mode
// All secrets are available except those in the Exclude array (when InheritAll is true)
func (sr *SecretResolver) resolveExcludeMode(allSecrets map[string]string, envConfig SecretsConfigMap) (map[string]string, error) {
result := make(map[string]string)

if envConfig.InheritAll {
// Copy all secrets first
for k, v := range allSecrets {
result[k] = v
}
}

// Remove excluded secrets
for _, ref := range envConfig.Exclude {
key := sr.extractKeyFromReference(ref)
delete(result, key)
}

return result, nil
}

// resolveOverrideMode resolves secrets in override mode
// Secrets can be literal values or references to other secrets
func (sr *SecretResolver) resolveOverrideMode(allSecrets map[string]string, envConfig SecretsConfigMap) (map[string]string, error) {
result := make(map[string]string)

for key, refOrValue := range envConfig.Override {
value, err := sr.resolveSecretReference(refOrValue, allSecrets)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve override for secret %q", key)
}
result[key] = value
}

return result, nil
}

// resolveSecretReference resolves a secret reference which can be:
// 1. Direct reference: "SECRET_NAME" or "~SECRET_NAME" - fetch directly from allSecrets
// 2. Mapped reference: "${secret:OTHER_SECRET}" - fetch OTHER_SECRET from allSecrets
// 3. Literal value: any other string - use as-is
func (sr *SecretResolver) resolveSecretReference(ref string, allSecrets map[string]string) (string, error) {
// Check for mapped reference pattern ${secret:KEY}
if matches := sr.secretRef.FindStringSubmatch(ref); matches != nil {
// Mapped reference - fetch the referenced secret
referencedKey := matches[1]
value, ok := allSecrets[referencedKey]
if !ok {
return "", errors.Errorf("referenced secret %q not found", referencedKey)
}
return value, nil
}

// Check for direct reference with ~ prefix
if strings.HasPrefix(ref, "~") {
// Direct reference - remove the ~ and fetch
key := strings.TrimPrefix(ref, "~")
value, ok := allSecrets[key]
if !ok {
return "", errors.Errorf("secret %q not found", key)
}
return value, nil
}

// Check if ref exists as a key in allSecrets (plain reference)
if value, ok := allSecrets[ref]; ok {
return value, nil
}

// Otherwise, treat as literal value
return ref, nil
}

// extractKeyFromReference extracts the secret key from a reference string
// For "SECRET_NAME" or "~SECRET_NAME" -> "SECRET_NAME"
// For "${secret:OTHER_SECRET}" -> the key that would reference this (not the referenced key)
func (sr *SecretResolver) extractKeyFromReference(ref string) string {
// If it's a mapped reference, we need to determine what key this would be stored as
// For simplicity, we use the reference string itself as the key
if matches := sr.secretRef.FindStringSubmatch(ref); matches != nil {
// This is a mapped reference - the key depends on context
// For include mode, the key is the reference itself
return ref
}

// For ~ prefix, remove it
if strings.HasPrefix(ref, "~") {
return strings.TrimPrefix(ref, "~")
}

return ref
}

// GetAvailableSecrets returns the list of secret keys that will be available for the given environment
func (sr *SecretResolver) GetAvailableSecrets(allSecrets map[string]string, env string) ([]string, error) {
if sr.config == nil {
// No filtering, all secrets are available
keys := make([]string, 0, len(allSecrets))
for k := range allSecrets {
keys = append(keys, k)
}
return keys, nil
}

envConfig, ok := sr.config.Secrets[env]
if !ok {
// No config for this environment, all secrets are available
keys := make([]string, 0, len(allSecrets))
for k := range allSecrets {
keys = append(keys, k)
}
return keys, nil
}

switch sr.config.Mode {
case "include":
result := make([]string, 0, len(envConfig.Include))
for _, ref := range envConfig.Include {
key := sr.extractKeyFromReference(ref)
result = append(result, key)
}
return result, nil
case "exclude":
if envConfig.InheritAll {
result := make([]string, 0, len(allSecrets))
for k := range allSecrets {
excluded := false
for _, excRef := range envConfig.Exclude {
excKey := sr.extractKeyFromReference(excRef)
if k == excKey {
excluded = true
break
}
}
if !excluded {
result = append(result, k)
}
}
return result, nil
}
// If not inheriting all, only include non-excluded secrets
return []string{}, nil
case "override":
result := make([]string, 0, len(envConfig.Override))
for k := range envConfig.Override {
result = append(result, k)
}
return result, nil
default:
return nil, errors.Errorf("unknown secrets config mode: %q", sr.config.Mode)
}
}
Loading
Loading