Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add envfile support #2042

Closed
wants to merge 4 commits into from

Conversation

shellphy
Copy link

@shellphy shellphy commented Nov 3, 2024

Reason for This PR

It is easier to configure environment variables in the.env file, Reduce the maintenance of configuration files

Description of Changes

It is implemented by variable substitution at the entry.

add .env file:

APP_ENV=production
LOG_LEVEL=warning

add variable in rr.yml:

envfile: .env
logs:
  mode: ${APP_ENV:-developement}
  level: ${LOG_LEVEL:-debug}

run rr serve -c rr.yml, then a temp config file will be used by rr :

envfile: .env
logs:
  mode: production
  level: warning

License Acceptance

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

PR Checklist

[Author TODO: Meet these criteria.]
[Reviewer TODO: Verify that these criteria are met. Request changes if not]

  • All commits in this PR are signed (git commit -s).
  • The reason for this PR is clearly provided (issue no. or explanation).
  • The description of changes is clear and encompassing.
  • Any required documentation changes (code and docs) are included in this PR.
  • Any user-facing changes are mentioned in CHANGELOG.md.
  • All added/changed functionality is tested.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Enhanced configuration management by introducing dynamic processing of configuration files and environment variables.
    • Added functionality to expand environment variables within configuration inputs.
    • Improved server initialization process with better handling of YAML configuration files and optional environment files.
    • Introduced error handling for configuration processing to ensure robust command execution.

Copy link
Contributor

coderabbitai bot commented Nov 3, 2024

Caution

Review failed

The pull request is closed.

Walkthrough

The changes in internal/cli/serve/command.go, container/config.go, and lib/roadrunner.go enhance configuration management by integrating new functionalities for processing configuration files and expanding environment variables. A new function, expandEnvVars, replaces environment variable occurrences in strings, while ProcessConfig reads configuration files and handles optional environment files. The NewCommand and NewRR functions are updated to utilize these new capabilities, improving error handling and ensuring temporary files are managed correctly.

Changes

File Change Summary
internal/cli/serve/command.go Enhanced NewCommand to call container.ProcessConfig for configuration processing. Updated error handling and import statement order.
container/config.go Added imports for godotenv and yaml. Introduced expandEnvVars(input string) string and ProcessConfig(cfgFile string) (string, error). Defined envVarPattern for environment variable matching.
lib/roadrunner.go Modified NewRR to call container.ProcessConfig(cfgFile) for configuration processing and improved error handling. Added import for os.

Poem

🐰 In the land of code where bunnies play,
New functions hop in to brighten the day.
Configs and envs, all neatly arranged,
With patterns and parsing, the flow's been changed.
So let’s cheer for the code, with a hop and a twirl,
For every new feature makes our hearts whirl! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (5)
internal/cli/serve/command.go (5)

81-84: Wrap the error when loading the environment file

When loading the environment file using godotenv.Load(envFile), any error is returned directly. For better error context and diagnosis, wrap the error to include additional information.

Apply this diff to wrap the error:

 err = godotenv.Load(envFile)
 if err != nil {
-    return "", err
+    return "", errors.E(op, fmt.Errorf("could not load environment file: %w", err))
 }

28-53: Improve robustness of environment variable parsing

In the expandEnvVars function, slicing operations like match[2:len(match)-1] may cause index out-of-range errors if unexpected patterns are encountered. Consider adding checks to ensure the slices are safe and handle any edge cases.

Alternatively, you can simplify environment variable expansion by using Go's built-in os.Expand or os.ExpandEnv functions, which handle variable substitution and default values safely.

Apply this diff to use os.Expand:

-func expandEnvVars(input string) string {
-    return envVarPattern.ReplaceAllStringFunc(input, func(match string) string {
-        // ... existing code ...
-    })
+func expandEnvVars(input string) string {
+    return os.Expand(input, func(varName string) string {
+        return os.Getenv(varName)
+    })
 }

71-74: Handle nested configurations when locating 'envfile'

The current implementation checks for envfile at the root level of the configuration map. If the configuration uses nested structures, this approach might miss envfile declarations located deeper in the hierarchy. Consider recursively searching the configuration map to locate envfile entries.


5-16: Organize and optimize imports for better readability

The import statements can be organized into standard library, third-party, and local packages to enhance readability. Also, remove any unused imports to clean up the code.

Arrange the imports as follows:

 import (
+    "fmt"
+    "os"
+    "os/signal"
+    "path/filepath"
+    "regexp"
+    "strings"
+    "syscall"
+
     "github.com/joho/godotenv"
     "github.com/roadrunner-server/endure/v2"
     "github.com/roadrunner-server/roadrunner/v2024/container"
     "github.com/roadrunner-server/roadrunner/v2024/internal/meta"
     "github.com/roadrunner-server/roadrunner/v2024/internal/sdnotify"
-    "gopkg.in/yaml.v3"
-    "os"
-    "os/signal"
-    "path/filepath"
-    "regexp"
-    "strings"
-    "syscall"
+    "gopkg.in/yaml.v3"
+
     configImpl "github.com/roadrunner-server/config/v5"
     "github.com/roadrunner-server/errors"
     "github.com/spf13/cobra"
 )

115-124: Ensure temporary config file is always cleaned up

The temporary config file is scheduled for removal with a defer statement, but only if len(tempFile) > 0. To prevent any potential file leakage, ensure that the deferred removal is called in all cases where a temporary file is created.

Move the defer statement immediately after the temporary file is created:

 tempFile, err := processConfig(*cfgFile)
 if err != nil {
     return errors.E(op, err)
 }
 if len(tempFile) > 0 {
     *cfgFile = tempFile
+    defer func() {
+        _ = os.Remove(tempFile)
+    }()
-    defer func() {
-        _ = os.Remove(tempFile)
-    }()
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3911515 and e654a87.

📒 Files selected for processing (1)
  • internal/cli/serve/command.go (2 hunks)
🔇 Additional comments (1)
internal/cli/serve/command.go (1)

87-88: Verify that environment variable expansion is secure

Environment variable expansion might introduce security risks if untrusted input is used or if the environment variables contain sensitive data. Confirm that the configuration files are from trusted sources and that the expanded content does not expose or log sensitive information.

Run the following script to check for any occurrences of potentially sensitive variables:

✅ Verification successful

Let me analyze the expandEnvVars function and its usage to better understand the security implications.


Let me check the complete implementation of the environment variable expansion and the pattern used.


Let me check how the expanded content is used after processing and if there's any logging of the configuration.


Based on the gathered information, I can now provide a final response.

Environment variable expansion implementation appears secure

The implementation shows good security practices:

  • Uses a well-defined regex pattern that only matches valid environment variable formats (${var}, $var, ${var:-default})
  • Expanded content is only written to a temporary file with restricted permissions (0644)
  • No logging of expanded configuration content
  • Configuration is read from a file and processed in memory without exposing sensitive data
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for usage of sensitive environment variables in the config file.

# Sensitive variable patterns (e.g., passwords, tokens, secrets)
sensitive_patterns='(PASSWORD|PASS|TOKEN|SECRET|KEY)'

# Search the expanded configuration content
grep -E "$sensitive_patterns" "$tempFile" && echo "Sensitive variables found in the configuration." || echo "No sensitive variables found."

Length of output: 206


Script:

#!/bin/bash
# Check the implementation of expandEnvVars and its usage
ast-grep --pattern 'func expandEnvVars($_) {
  $$$
}'

# Also check where this function is used
rg "expandEnvVars" -A 3

Length of output: 765


Script:

#!/bin/bash
# Check the environment variable pattern and full implementation
rg "envVarPattern" -B 2 -A 10

# Also check how the content is being read and used
rg "content.*:=.*" -A 3 --context-separator="" internal/cli/serve/command.go

Length of output: 1353


Script:

#!/bin/bash
# Check how the expanded content is used
rg "expandedContent" -B 2 -A 5

# Check for any logging of configuration content
rg -i "log|print|fmt.+content" internal/cli/serve/command.go

Length of output: 1781

Comment on lines 90 to 94
tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
if err != nil {
return "", errors.E(op, fmt.Errorf("could not create temporary config file: %w", err))
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Close the temporary file to prevent resource leakage

The temporary file tmpFile is created with os.CreateTemp but is never closed, which can lead to file descriptor leakage. Ensure that you close the file after writing to it.

Apply this diff to close the temporary file:

 tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
 if err != nil {
     return "", errors.E(op, fmt.Errorf("could not create temporary config file: %w", err))
 }
+defer func() {
+    _ = tmpFile.Close()
+}()

 if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
     return "", errors.E(op, fmt.Errorf("could not write processed config: %w", err))
 }

Alternatively, you can write directly to the temporary file and avoid using os.WriteFile:

 tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
 if err != nil {
     return "", errors.E(op, fmt.Errorf("could not create temporary config file: %w", err))
 }
+defer func() {
+    _ = tmpFile.Close()
+}()

-if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
+if _, err = tmpFile.Write([]byte(expandedContent)); err != nil {
     return "", errors.E(op, fmt.Errorf("could not write processed config: %w", err))
 }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (2)
internal/cli/serve/command.go (2)

23-24: Consider pre-compiling the regex pattern at init time.

The envVarPattern regex is complex and used frequently. While it's already package-level, consider initializing it in an init() function to ensure proper error handling during compilation.

-var envVarPattern = regexp.MustCompile(`\$\{([^{}:\-]+)(?::-([^{}]+))?\}|\$([A-Za-z0-9_]+)`)
+var envVarPattern *regexp.Regexp
+
+func init() {
+    var err error
+    envVarPattern, err = regexp.Compile(`\$\{([^{}:\-]+)(?::-([^{}]+))?\}|\$([A-Za-z0-9_]+)`)
+    if err != nil {
+        panic(fmt.Sprintf("failed to compile env var pattern: %v", err))
+    }
+}

115-125: Improve error context in processConfig error handling.

While the error handling is functional, it could provide more context about the operation that failed.

 // Process config and get temporary file path
 tempFile, err := processConfig(*cfgFile)
 if err != nil {
-    return errors.E(op, err)
+    return errors.E(op, fmt.Errorf("failed to process config file %q: %w", *cfgFile, err))
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e654a87 and 02cda15.

📒 Files selected for processing (1)
  • internal/cli/serve/command.go (2 hunks)
🔇 Additional comments (1)
internal/cli/serve/command.go (1)

27-54: LGTM! The environment variable expansion logic is robust.

The implementation correctly handles:

  • Variables with default values ${VAR:-default}
  • Simple variables ${VAR} or $VAR
  • Fallback to original string if variable not found

Comment on lines 96 to 98
if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
return "", err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review security implications of temporary file permissions.

The temporary configuration file is created with 0644 permissions, which might expose sensitive environment variables to other users on the system. Consider using more restrictive permissions.

-if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
+if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0600); err != nil {
     return "", err
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
return "", err
}
if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0600); err != nil {
return "", err
}

Comment on lines 75 to 82
if !filepath.IsAbs(envFile) {
envFile = filepath.Join(filepath.Dir(cfgFile), envFile)
}

err = godotenv.Load(envFile)
if err != nil {
return "", err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add existence check for the env file before loading.

The code should verify if the env file exists before attempting to load it. This would provide a more user-friendly error message.

 if !filepath.IsAbs(envFile) {
     envFile = filepath.Join(filepath.Dir(cfgFile), envFile)
 }

+if _, err := os.Stat(envFile); err != nil {
+    if os.IsNotExist(err) {
+        return "", fmt.Errorf("env file does not exist: %s", envFile)
+    }
+    return "", fmt.Errorf("error accessing env file: %w", err)
+}
+
 err = godotenv.Load(envFile)
 if err != nil {
     return "", err
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !filepath.IsAbs(envFile) {
envFile = filepath.Join(filepath.Dir(cfgFile), envFile)
}
err = godotenv.Load(envFile)
if err != nil {
return "", err
}
if !filepath.IsAbs(envFile) {
envFile = filepath.Join(filepath.Dir(cfgFile), envFile)
}
if _, err := os.Stat(envFile); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("env file does not exist: %s", envFile)
}
return "", fmt.Errorf("error accessing env file: %w", err)
}
err = godotenv.Load(envFile)
if err != nil {
return "", err
}

Comment on lines 87 to 101
// Create temporary file with processed content
tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
if err != nil {
return "", err
}
defer func() {
_ = tmpFile.Close()
}()

if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
return "", err
}

return tmpFile.Name(), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure temporary file cleanup on all error paths.

While the temporary file is properly closed with defer, it should be cleaned up if an error occurs during writing.

 // Create temporary file with processed content
 tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
 if err != nil {
     return "", err
 }
+tmpName := tmpFile.Name()
 defer func() {
     _ = tmpFile.Close()
+    if err != nil {
+        _ = os.Remove(tmpName)
+    }
 }()

-if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
+if err = os.WriteFile(tmpName, []byte(expandedContent), 0644); err != nil {
     return "", err
 }

-return tmpFile.Name(), nil
+return tmpName, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create temporary file with processed content
tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
if err != nil {
return "", err
}
defer func() {
_ = tmpFile.Close()
}()
if err = os.WriteFile(tmpFile.Name(), []byte(expandedContent), 0644); err != nil {
return "", err
}
return tmpFile.Name(), nil
}
// Create temporary file with processed content
tmpFile, err := os.CreateTemp("", "rr-processed-*.yaml")
if err != nil {
return "", err
}
tmpName := tmpFile.Name()
defer func() {
_ = tmpFile.Close()
if err != nil {
_ = os.Remove(tmpName)
}
}()
if err = os.WriteFile(tmpName, []byte(expandedContent), 0644); err != nil {
return "", err
}
return tmpName, nil
}

@rustatian rustatian self-requested a review November 3, 2024 10:23
@rustatian rustatian added the C-enhancement Category: enhancement. Meaning improvements of current module, transport, etc.. label Nov 3, 2024
@rustatian rustatian changed the title Add envfile support feature: add envfile support Nov 3, 2024
Copy link

codecov bot commented Nov 3, 2024

Codecov Report

Attention: Patch coverage is 6.84932% with 68 lines in your changes missing coverage. Please review.

Project coverage is 33.66%. Comparing base (dfe53b9) to head (aff113a).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
container/config.go 0.00% 54 Missing ⚠️
internal/cli/serve/command.go 0.00% 9 Missing ⚠️
lib/roadrunner.go 50.00% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2042      +/-   ##
==========================================
- Coverage   35.95%   33.66%   -2.29%     
==========================================
  Files          17       17              
  Lines        1018     1090      +72     
==========================================
+ Hits          366      367       +1     
- Misses        621      690      +69     
- Partials       31       33       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@rustatian
Copy link
Member

Hey @shellphy 👋
Thank you for the PR 👍
However, RR already supports envfiles: link.

@rustatian rustatian closed this Nov 3, 2024
@shellphy
Copy link
Author

shellphy commented Nov 3, 2024

I see that you support envfiles, but there are very few environment variables supported now, and it is not used to replace the configuration in rr.yml, my implementation is a more general scheme, can be more flexible, I think it is necessary, otherwise I need to maintain multiple rr.yml configuration files

@rustatian
Copy link
Member

All envs are supported and used to replace placeholders, see configuration (config) plugin. Your implementation has several critical errors:

  1. Writing tmp config (why?) under some predefined permissions. Configuration might contain sensitive info.
  2. You don't need to use regex to parse envs.

@rustatian
Copy link
Member

  1. You don't need to maintain multiply .rr.yaml configurations. Just use includes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: enhancement. Meaning improvements of current module, transport, etc..
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants