Change to defaultSecrets
This commit is contained in:
@@ -85,7 +85,7 @@ func (api *API) AppsAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Parse request
|
// Parse request
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Config map[string]string `json:"config"`
|
Config map[string]interface{} `json:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
"github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles application lifecycle operations
|
// Manager handles application lifecycle operations
|
||||||
@@ -40,7 +41,7 @@ type App struct {
|
|||||||
Dependencies []string `json:"dependencies" yaml:"dependencies"`
|
Dependencies []string `json:"dependencies" yaml:"dependencies"`
|
||||||
Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"`
|
Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"`
|
||||||
DefaultConfig map[string]interface{} `json:"defaultConfig,omitempty" yaml:"defaultConfig,omitempty"`
|
DefaultConfig map[string]interface{} `json:"defaultConfig,omitempty" yaml:"defaultConfig,omitempty"`
|
||||||
RequiredSecrets []string `json:"requiredSecrets,omitempty" yaml:"requiredSecrets,omitempty"`
|
DefaultSecrets []string `json:"defaultSecrets,omitempty" yaml:"defaultSecrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeployedApp represents a deployed application instance
|
// DeployedApp represents a deployed application instance
|
||||||
@@ -95,7 +96,14 @@ func (m *Manager) ListAvailable() ([]App, error) {
|
|||||||
Category: manifest.Category,
|
Category: manifest.Category,
|
||||||
Icon: manifest.Icon,
|
Icon: manifest.Icon,
|
||||||
DefaultConfig: manifest.DefaultConfig,
|
DefaultConfig: manifest.DefaultConfig,
|
||||||
RequiredSecrets: manifest.RequiredSecrets,
|
}
|
||||||
|
|
||||||
|
// Extract secret keys from DefaultSecrets
|
||||||
|
if len(manifest.DefaultSecrets) > 0 {
|
||||||
|
app.DefaultSecrets = make([]string, len(manifest.DefaultSecrets))
|
||||||
|
for i, secret := range manifest.DefaultSecrets {
|
||||||
|
app.DefaultSecrets[i] = secret.Key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract dependencies from Requires field
|
// Extract dependencies from Requires field
|
||||||
@@ -138,7 +146,14 @@ func (m *Manager) Get(appName string) (*App, error) {
|
|||||||
Category: manifest.Category,
|
Category: manifest.Category,
|
||||||
Icon: manifest.Icon,
|
Icon: manifest.Icon,
|
||||||
DefaultConfig: manifest.DefaultConfig,
|
DefaultConfig: manifest.DefaultConfig,
|
||||||
RequiredSecrets: manifest.RequiredSecrets,
|
}
|
||||||
|
|
||||||
|
// Extract secret keys from DefaultSecrets
|
||||||
|
if len(manifest.DefaultSecrets) > 0 {
|
||||||
|
app.DefaultSecrets = make([]string, len(manifest.DefaultSecrets))
|
||||||
|
for i, secret := range manifest.DefaultSecrets {
|
||||||
|
app.DefaultSecrets[i] = secret.Key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract dependencies from Requires field
|
// Extract dependencies from Requires field
|
||||||
@@ -282,8 +297,104 @@ func (m *Manager) ListDeployed(instanceName string) ([]DeployedApp, error) {
|
|||||||
return apps, nil
|
return apps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// processConfigTemplates recursively processes gomplate templates in config values
|
||||||
|
// This function expects config values at the root context (e.g., {{ .cloud.domain }})
|
||||||
|
func processConfigTemplates(config map[string]interface{}, configFile string, gomplate *tools.Gomplate) (map[string]interface{}, error) {
|
||||||
|
processed := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, value := range config {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
// Process string templates
|
||||||
|
if strings.Contains(v, "{{") {
|
||||||
|
// Use gomplate with config at root context
|
||||||
|
args := []string{
|
||||||
|
"-i", v,
|
||||||
|
"-c", fmt.Sprintf(".=%s", configFile),
|
||||||
|
}
|
||||||
|
compiled, err := gomplate.Exec(args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to compile template for key %s: %w", key, err)
|
||||||
|
}
|
||||||
|
processed[key] = strings.TrimSpace(compiled)
|
||||||
|
} else {
|
||||||
|
processed[key] = v
|
||||||
|
}
|
||||||
|
case map[string]interface{}:
|
||||||
|
// Recursively process nested maps
|
||||||
|
nestedProcessed, err := processConfigTemplates(v, configFile, gomplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
processed[key] = nestedProcessed
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
// Convert to string map and process
|
||||||
|
stringMap := make(map[string]interface{})
|
||||||
|
for k, val := range v {
|
||||||
|
if strKey, ok := k.(string); ok {
|
||||||
|
stringMap[strKey] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nestedProcessed, err := processConfigTemplates(stringMap, configFile, gomplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
processed[key] = nestedProcessed
|
||||||
|
default:
|
||||||
|
// Keep other types as-is
|
||||||
|
processed[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSecretTemplate processes a gomplate template for secret defaults
|
||||||
|
// This function uses named contexts for config and secrets (e.g., {{ .config.apps.loomio.db.user }}, {{ .secrets.apps.loomio.dbPassword }})
|
||||||
|
func processSecretTemplate(template string, configFile, secretsFile string, gomplate *tools.Gomplate) (string, error) {
|
||||||
|
args := []string{
|
||||||
|
"-i", template,
|
||||||
|
"-c", fmt.Sprintf("config=%s", configFile),
|
||||||
|
"-c", fmt.Sprintf("secrets=%s", secretsFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
compiled, err := gomplate.Exec(args...)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to process template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(compiled), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setNestedConfig recursively sets nested configuration values using yq
|
||||||
|
func setNestedConfig(yq *tools.YQ, configFile, basePath string, value interface{}) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
// Handle nested map - recursively set each field
|
||||||
|
for k, nested := range v {
|
||||||
|
nestedPath := fmt.Sprintf("%s.%s", basePath, k)
|
||||||
|
if err := setNestedConfig(yq, configFile, nestedPath, nested); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
// YAML unmarshaling sometimes produces this type
|
||||||
|
for k, nested := range v {
|
||||||
|
key := fmt.Sprintf("%v", k)
|
||||||
|
nestedPath := fmt.Sprintf("%s.%s", basePath, key)
|
||||||
|
if err := setNestedConfig(yq, configFile, nestedPath, nested); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Primitive value - set it directly
|
||||||
|
return yq.Set(configFile, basePath, fmt.Sprintf("%v", value))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Add adds an app to the instance configuration
|
// Add adds an app to the instance configuration
|
||||||
func (m *Manager) Add(instanceName, appName string, config map[string]string) error {
|
func (m *Manager) Add(instanceName, appName string, config map[string]interface{}) error {
|
||||||
// 1. Verify app exists
|
// 1. Verify app exists
|
||||||
manifestPath := filepath.Join(m.appsDir, appName, "manifest.yaml")
|
manifestPath := filepath.Join(m.appsDir, appName, "manifest.yaml")
|
||||||
if !storage.FileExists(manifestPath) {
|
if !storage.FileExists(manifestPath) {
|
||||||
@@ -300,33 +411,28 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
return fmt.Errorf("instance config not found: %s", instanceName)
|
return fmt.Errorf("instance config not found: %s", instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Process manifest with gomplate
|
// 2. Parse manifest (without processing templates in defaultSecrets)
|
||||||
tempManifest := filepath.Join(os.TempDir(), fmt.Sprintf("manifest-%s.yaml", appName))
|
manifestData, err := os.ReadFile(manifestPath)
|
||||||
defer os.Remove(tempManifest)
|
|
||||||
|
|
||||||
gomplate := tools.NewGomplate()
|
|
||||||
context := map[string]string{
|
|
||||||
".": configFile,
|
|
||||||
"secrets": secretsFile,
|
|
||||||
}
|
|
||||||
if err := gomplate.RenderWithContext(manifestPath, tempManifest, context); err != nil {
|
|
||||||
return fmt.Errorf("failed to process manifest: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse processed manifest
|
|
||||||
manifestData, err := os.ReadFile(tempManifest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read processed manifest: %w", err)
|
return fmt.Errorf("failed to read manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifest struct {
|
var manifest AppManifest
|
||||||
DefaultConfig map[string]interface{} `yaml:"defaultConfig"`
|
|
||||||
RequiredSecrets []string `yaml:"requiredSecrets"`
|
|
||||||
}
|
|
||||||
if err := yaml.Unmarshal(manifestData, &manifest); err != nil {
|
if err := yaml.Unmarshal(manifestData, &manifest); err != nil {
|
||||||
return fmt.Errorf("failed to parse manifest: %w", err)
|
return fmt.Errorf("failed to parse manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gomplate := tools.NewGomplate()
|
||||||
|
|
||||||
|
// Process defaultConfig templates recursively
|
||||||
|
if manifest.DefaultConfig != nil {
|
||||||
|
processedConfig, err := processConfigTemplates(manifest.DefaultConfig, configFile, gomplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process config templates: %w", err)
|
||||||
|
}
|
||||||
|
manifest.DefaultConfig = processedConfig
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Update configuration
|
// 3. Update configuration
|
||||||
yq := tools.NewYQ()
|
yq := tools.NewYQ()
|
||||||
configLock := configFile + ".lock"
|
configLock := configFile + ".lock"
|
||||||
@@ -345,7 +451,8 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
// Only set if not already present
|
// Only set if not already present
|
||||||
existing, _ := yq.Get(configFile, keyPath)
|
existing, _ := yq.Get(configFile, keyPath)
|
||||||
if existing == "" || existing == "null" {
|
if existing == "" || existing == "null" {
|
||||||
if err := yq.Set(configFile, keyPath, fmt.Sprintf("%v", value)); err != nil {
|
// Use our helper function to handle nested objects properly
|
||||||
|
if err := setNestedConfig(yq, configFile, keyPath, value); err != nil {
|
||||||
return fmt.Errorf("failed to set config %s: %w", key, err)
|
return fmt.Errorf("failed to set config %s: %w", key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,7 +462,8 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
// Apply user-provided config overrides
|
// Apply user-provided config overrides
|
||||||
for key, value := range config {
|
for key, value := range config {
|
||||||
keyPath := fmt.Sprintf(".apps.%s.%s", appName, key)
|
keyPath := fmt.Sprintf(".apps.%s.%s", appName, key)
|
||||||
if err := yq.Set(configFile, keyPath, value); err != nil {
|
// Use setNestedConfig to handle both simple values and nested objects
|
||||||
|
if err := setNestedConfig(yq, configFile, keyPath, value); err != nil {
|
||||||
return fmt.Errorf("failed to set config %s: %w", key, err)
|
return fmt.Errorf("failed to set config %s: %w", key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,10 +474,42 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Generate required secrets
|
// 4. Generate required secrets
|
||||||
|
// Process secrets sequentially so later ones can reference earlier ones
|
||||||
secretsMgr := secrets.NewManager()
|
secretsMgr := secrets.NewManager()
|
||||||
for _, secretKey := range manifest.RequiredSecrets {
|
|
||||||
if _, err := secretsMgr.EnsureSecret(secretsFile, secretKey, secrets.DefaultSecretLength); err != nil {
|
for _, secretDef := range manifest.DefaultSecrets {
|
||||||
return fmt.Errorf("failed to ensure secret %s: %w", secretKey, err)
|
// Check if secret already exists
|
||||||
|
existingSecret, _ := secretsMgr.GetSecret(secretsFile, secretDef.Key)
|
||||||
|
if existingSecret != "" && existingSecret != "null" {
|
||||||
|
// Secret already exists, don't overwrite
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretValue string
|
||||||
|
|
||||||
|
if secretDef.Default != "" {
|
||||||
|
// Process the default value using gomplate
|
||||||
|
if strings.Contains(secretDef.Default, "{{") {
|
||||||
|
compiled, err := processSecretTemplate(secretDef.Default, configFile, secretsFile, gomplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compile secret template for %s: %w", secretDef.Key, err)
|
||||||
|
}
|
||||||
|
secretValue = compiled
|
||||||
|
} else {
|
||||||
|
secretValue = secretDef.Default
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No default specified, generate random secret
|
||||||
|
var err error
|
||||||
|
secretValue, err = secrets.GenerateSecret(secrets.DefaultSecretLength)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate secret for %s: %w", secretDef.Key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the secret value
|
||||||
|
if err := secretsMgr.SetSecret(secretsFile, secretDef.Key, secretValue); err != nil {
|
||||||
|
return fmt.Errorf("failed to set secret %s: %w", secretDef.Key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +525,12 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
return fmt.Errorf("failed to read app directory: %w", err)
|
return fmt.Errorf("failed to read app directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create context for template processing
|
||||||
|
context := map[string]string{
|
||||||
|
".": configFile,
|
||||||
|
"secrets": secretsFile,
|
||||||
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
// TODO: Handle subdirectories if needed
|
// TODO: Handle subdirectories if needed
|
||||||
@@ -394,7 +540,21 @@ func (m *Manager) Add(instanceName, appName string, config map[string]string) er
|
|||||||
sourcePath := filepath.Join(sourceAppDir, entry.Name())
|
sourcePath := filepath.Join(sourceAppDir, entry.Name())
|
||||||
destPath := filepath.Join(appDestDir, entry.Name())
|
destPath := filepath.Join(appDestDir, entry.Name())
|
||||||
|
|
||||||
// Process with gomplate
|
// Skip processing manifest.yaml - it contains secret template definitions
|
||||||
|
// that should not be processed. We've already extracted and used what we need from it.
|
||||||
|
if entry.Name() == "manifest.yaml" {
|
||||||
|
// Just copy it as-is
|
||||||
|
data, err := os.ReadFile(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", entry.Name(), err)
|
||||||
|
}
|
||||||
|
if err := storage.WriteFile(destPath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", entry.Name(), err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process other files with gomplate
|
||||||
if err := gomplate.RenderWithContext(sourcePath, destPath, context); err != nil {
|
if err := gomplate.RenderWithContext(sourcePath, destPath, context); err != nil {
|
||||||
return fmt.Errorf("failed to compile %s: %w", entry.Name(), err)
|
return fmt.Errorf("failed to compile %s: %w", entry.Name(), err)
|
||||||
}
|
}
|
||||||
@@ -425,6 +585,50 @@ func (m *Manager) Deploy(instanceName, appName string) error {
|
|||||||
tools.WithKubeconfig(applyNsCmd, kubeconfigPath)
|
tools.WithKubeconfig(applyNsCmd, kubeconfigPath)
|
||||||
_, _ = applyNsCmd.CombinedOutput() // Ignore errors - namespace might already exist
|
_, _ = applyNsCmd.CombinedOutput() // Ignore errors - namespace might already exist
|
||||||
|
|
||||||
|
// Check if this app needs postgres-secrets (for db-init jobs)
|
||||||
|
// Look for db-init-job.yaml that references postgres-secrets
|
||||||
|
dbInitJobPath := filepath.Join(appDir, "db-init-job.yaml")
|
||||||
|
if storage.FileExists(dbInitJobPath) {
|
||||||
|
dbInitContent, _ := os.ReadFile(dbInitJobPath)
|
||||||
|
if bytes.Contains(dbInitContent, []byte("postgres-secrets")) {
|
||||||
|
// Copy postgres-secrets from postgres namespace to app namespace
|
||||||
|
getSecretCmd := exec.Command("kubectl", "get", "secret", "postgres-secrets",
|
||||||
|
"-n", "postgres", "-o", "yaml")
|
||||||
|
tools.WithKubeconfig(getSecretCmd, kubeconfigPath)
|
||||||
|
secretYaml, err := getSecretCmd.CombinedOutput()
|
||||||
|
if err == nil {
|
||||||
|
// Replace namespace in the YAML
|
||||||
|
secretYaml = bytes.ReplaceAll(secretYaml, []byte("namespace: postgres"),
|
||||||
|
[]byte(fmt.Sprintf("namespace: %s", appName)))
|
||||||
|
|
||||||
|
// Apply the secret to the app namespace
|
||||||
|
applySecretCmd := exec.Command("kubectl", "apply", "-f", "-")
|
||||||
|
applySecretCmd.Stdin = bytes.NewReader(secretYaml)
|
||||||
|
tools.WithKubeconfig(applySecretCmd, kubeconfigPath)
|
||||||
|
_, _ = applySecretCmd.CombinedOutput() // Ignore errors if secret already exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this app has an ingress with TLS configuration
|
||||||
|
// and copy wildcard certificates from cert-manager namespace
|
||||||
|
ingressPath := filepath.Join(appDir, "ingress.yaml")
|
||||||
|
if storage.FileExists(ingressPath) {
|
||||||
|
ingressContent, _ := os.ReadFile(ingressPath)
|
||||||
|
// Check for wildcard TLS secrets that need to be copied from cert-manager
|
||||||
|
wildcardSecrets := []string{"wildcard-wild-cloud-tls", "wildcard-internal-wild-cloud-tls"}
|
||||||
|
for _, secretName := range wildcardSecrets {
|
||||||
|
if bytes.Contains(ingressContent, []byte(secretName)) {
|
||||||
|
// Copy the wildcard certificate from cert-manager namespace
|
||||||
|
if err := utilities.CopySecretBetweenNamespaces(kubeconfigPath, secretName, "cert-manager", appName); err != nil {
|
||||||
|
// Log error but don't fail deployment - TLS will use default cert
|
||||||
|
// This is non-fatal as the app will still work, just without proper TLS
|
||||||
|
fmt.Printf("Warning: Failed to copy TLS secret %s: %v\n", secretName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create Kubernetes secrets from secrets.yaml
|
// Create Kubernetes secrets from secrets.yaml
|
||||||
if storage.FileExists(secretsFile) {
|
if storage.FileExists(secretsFile) {
|
||||||
yq := tools.NewYQ()
|
yq := tools.NewYQ()
|
||||||
|
|||||||
1019
internal/apps/apps_test.go
Normal file
1019
internal/apps/apps_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,12 @@ package apps
|
|||||||
|
|
||||||
import "github.com/wild-cloud/wild-central/daemon/internal/tools"
|
import "github.com/wild-cloud/wild-central/daemon/internal/tools"
|
||||||
|
|
||||||
|
// SecretDefinition represents a secret with optional default value
|
||||||
|
type SecretDefinition struct {
|
||||||
|
Key string `json:"key" yaml:"key"`
|
||||||
|
Default string `json:"default,omitempty" yaml:"default,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// AppManifest represents the complete app manifest from manifest.yaml
|
// AppManifest represents the complete app manifest from manifest.yaml
|
||||||
type AppManifest struct {
|
type AppManifest struct {
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
@@ -11,7 +17,7 @@ type AppManifest struct {
|
|||||||
Category string `json:"category,omitempty" yaml:"category,omitempty"`
|
Category string `json:"category,omitempty" yaml:"category,omitempty"`
|
||||||
Requires []AppDependency `json:"requires,omitempty" yaml:"requires,omitempty"`
|
Requires []AppDependency `json:"requires,omitempty" yaml:"requires,omitempty"`
|
||||||
DefaultConfig map[string]interface{} `json:"defaultConfig,omitempty" yaml:"defaultConfig,omitempty"`
|
DefaultConfig map[string]interface{} `json:"defaultConfig,omitempty" yaml:"defaultConfig,omitempty"`
|
||||||
RequiredSecrets []string `json:"requiredSecrets,omitempty" yaml:"requiredSecrets,omitempty"`
|
DefaultSecrets []SecretDefinition `json:"defaultSecrets,omitempty" yaml:"defaultSecrets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppDependency represents a dependency on another app
|
// AppDependency represents a dependency on another app
|
||||||
|
|||||||
Reference in New Issue
Block a user