Skip to content

Commit

Permalink
Feature flag Release Doc (#4891)
Browse files Browse the repository at this point in the history
Doc to explain the feature flag release process.

Updates the rollout infra script to read the files to upload from github
instead of local. Also refactors the script to use helper functions.
  • Loading branch information
gmechali authored Jan 27, 2025
1 parent ee4c69a commit 615800a
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 59 deletions.
4 changes: 4 additions & 0 deletions docs/developer_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ TIPS: you can inspect variable in the botton of "DEBUG CONSOLE" window.
A full tutorial of debugging Flask app in Visual Studio Code is in
[here](https://code.visualstudio.com/docs/python/tutorial-flask).

### Manage Feature Flags

Feature flags are used to gate the rollout of features, and can easily be turned on/off in various environments. Please read the Feature Flags [guide](https://github.com/datacommonsorg/website/blob/master/docs/feature_flags.md).

### Add new charts in Place Page

1. Update [server/config/chart_config/](../server/config/chart_config)`<category>.json` with the new chart.
Expand Down
63 changes: 63 additions & 0 deletions docs/feature_flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Deploying Feature Flags

This script automates the deployment of feature flags to a Google Cloud Storage (GCS) bucket and optionally restarts a Kubernetes deployment.

## Usage

```bash
./deploy_feature_flags.sh <environment>
```


Where `<environment>` is one of:

* `dev`
* `staging`
* `production`
* `autopush`

## Environment Variables

* `GOOGLE_APPLICATION_CREDENTIALS`: The path to your Google Cloud credentials file.

## How it Works

1. **Validate the environment:** The script checks if the provided environment is one of the valid options (`dev`, `staging`, `production`, or `autopush`).
2. **Construct the filename:** The filename is created using the provided environment (e.g., `dev.json`, `staging.json`).
3. **Find the Python command:** The script searches for the Python interpreter (`python3` or `python`).
4. **Validate the JSON file:** It uses the Python `json.tool` module to check if the JSON file is valid.
5. **Construct the bucket name:** The bucket name is formed using the environment, with "datcom-website-" as a prefix and "prod-resources" for production.
6. **Confirm for production:** If deploying to production, it asks for confirmation from the user.
7. **Fetch staging flags (production only):** If deploying to production, it fetches the feature flags from the staging bucket (`datcom-website-staging-resources/feature_flags.json`).
8. **Compare staging and production flags (production only):** If deploying to production, it compares the staging and production flags and exits if there are differences.
9. **Upload the JSON file to GCS:** It uses the `gsutil` command to upload the JSON file to the appropriate GCS bucket.
10. **Prompt for Kubernetes restart:** It asks the user if they want to restart the Kubernetes deployment.
11. **Restart the Kubernetes deployment (optional):** If the user confirms, it uses `gcloud` commands to restart the Kubernetes deployment.

## GCS Buckets

The feature_flags.json file is uploaded to the following GCS buckets:
* Autopush: [datcom-website-autopush-resources](https://pantheon.corp.google.com/storage/browser/datcom-website-autopush-resources;tab=objects?e=13803378&mods=-monitoring_api_staging&project=datcom-ci)
* Dev: [datcom-website-dev-resources](https://pantheon.corp.google.com/storage/browser/datcom-website-dev-resources;tab=objects?e=13803378&mods=-monitoring_api_staging&project=datcom-ci)
* Staging: [datcom-website-staging-resources](https://pantheon.corp.google.com/storage/browser/datcom-website-staging-resources;tab=objects?e=13803378&mods=-monitoring_api_staging&project=datcom-ci&prefix=&forceOnObjectsSortingFiltering=false)
* Production: [datcom-website-prod-resources](https://pantheon.corp.google.com/storage/browser/datcom-website-prod-resources;tab=objects?e=13803378&mods=-monitoring_api_staging&project=datcom-ci&prefix=&forceOnObjectsSortingFiltering=false)

## Checked in Flag Files

This script uploads the feature flag configuration files from the Github master branch, into the following GCS Buckets above. You can find the flag configs in [`server/config/feature_flag_configs`](https://github.com/datacommonsorg/website/tree/9ed3b4aa8639056a410befcb0df1bc2373f33807/server/config/feature_flag_configs).

## How to Add a New Flag

1. **Add flag in flag configurations**: Check the flags in the Github master branch [`server/config/feature_flag_configs`](https://github.com/datacommonsorg/website/tree/master/server/config/feature_flag_configs).
2. **Define flag in server layer**: Add your flag constant to [feature_flags.py](https://github.com/datacommonsorg/website/blob/master/server/lib/feature_flags.py#L19) helper file for use in the API layer.
3. **Define flag in client layer**: Add your flag constant to [feature_flags/util.ts](https://github.com/datacommonsorg/website/blob/master/static/js/shared/feature_flags/util.ts#L18) helper file for use in the client layer.
4. **Check flag value and implement your feature**
5. **Deploy & update GCS Flag files**: Once your code reaches production, you can enable your flags and run the script to update the GCS flag files.
6. **Restart Kubernetes**: The script to update the GCS flag files will prompt you to restart Kubernetes, on restart the new flags will be applied and your feature will be enabled.


## Important Notes

* This script assumes you have the Google Cloud SDK installed and configured.
* The script expects a JSON file named `dev.json`, `staging.json`, or `production.json` in the current directory.
* The script can be modified to upload different files or perform additional actions after the deployment.
185 changes: 126 additions & 59 deletions scripts/update_gcs_feature_flags.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,92 +28,159 @@
# (3) Run `./scripts/update_gcs_feature_flags.sh <environment>`
# Where <environment> is one of: dev, staging, production, autopush

# Define the valid environments
valid_environments=("dev" "staging" "production" "autopush")
# Helper functions

# Function to check if an environment is valid
is_valid_environment() {
local environment="$1"
local valid_environments=("dev" "staging" "production" "autopush")
[[ ! " ${valid_environments[@]} " =~ " ${environment} " ]] && return 1 || return 0
}

# Function to find the correct Python executable
find_python() {
if command -v python3 &> /dev/null; then
echo "python3"
elif command -v python &> /dev/null; then
echo "python"
else
echo "Error: Python not found!"
exit 1
fi
}

# Function to download a file from GitHub
download_from_github() {
local file="$1"
local temp_file="$2"
local github_base_url="https://raw.githubusercontent.com/Datacommonsorg/website/master/server/config/feature_flag_configs"

echo "Downloading ${file} from GitHub..."
curl -sL "${github_base_url}/${file}" -o "${temp_file}" # Added -L for redirects

if [[ $? -ne 0 ]]; then
echo "Error: Failed to download ${file} from GitHub. curl exited with code $?."
curl -v "${github_base_url}/${file}" >&2 #Verbose curl output for debugging
return 1
fi
return 0
}

# Function to validate a JSON file
validate_json() {
local file="$1"
local python_executable=$(find_python) # Use the find_python function
if ! $python_executable -m json.tool "${file}" &> /dev/null; then
echo "Error: ${file} is not valid JSON."
return 1
fi
return 0
}

# Function to get the bucket name
get_bucket_name() {
local environment="$1"
if [[ "$environment" == "production" ]]; then
echo "datcom-website-prod-resources"
else
echo "datcom-website-${environment}-resources"
fi
}

# Function to compare staging and production flags
compare_staging_production() {
local temp_file="$1"
local staging_file="staging_flags.json"
local github_base_url="https://raw.githubusercontent.com/Datacommonsorg/website/master/server/config/feature_flag_configs"

echo "Fetching staging feature flags from GCS and Github..."
gsutil cp "gs://datcom-website-staging-resources/feature_flags.json" "${staging_file}"
curl -sL "${github_base_url}/staging.json" -o "staging_from_github.json"

if [[ $? -ne 0 ]]; then
echo "Error: Failed to download staging.json from GitHub."
return 1
fi

echo "Comparing staging and production feature flags..."
if ! diff --color "${temp_file}" "${staging_file}" &> /dev/null; then
echo "Error: Production feature flags differ from staging."
echo "Please ensure the flags are identical before deploying to production."
echo "Diffs:"
diff -C 2 --color "${temp_file}" "${staging_file}"
rm "${staging_file}"
rm "staging_from_github.json"
return 1
fi
rm "${staging_file}"
rm "staging_from_github.json"
return 0
}

# Function to restart the Kubernetes deployment
restart_kubernetes_deployment() {
local environment="$1"
gcloud config set project "datcom-website-${environment}"
gcloud container clusters get-credentials website-us-central1 --region us-central1 --project "datcom-website-${environment}"
kubectl rollout restart deployment website-app -n website
echo "Kubernetes deployment restarted."
}

# Check if an environment is provided
# Main script

# Check for environment argument
if [ -z "$1" ]; then
echo "Error: Please provide an environment as an argument."
echo "Usage: $0 <environment>"
exit 1
fi

# Get the environment from the command line argument
environment="$1"

# Check if the provided environment is valid
if [[ ! " ${valid_environments[@]} " =~ " ${environment} " ]]; then
# Validate the environment
if ! is_valid_environment "$environment"; then
echo "Error: Invalid environment '$environment'."
exit 1
fi

# Construct the filename (e.g., dev.json, staging.json)
file="${environment}.json"
temp_file="./temp.json"

# Find the right python command.
if command -v python3 &> /dev/null; then
PYTHON=python3
elif command -v python &> /dev/null; then
PYTHON=python
else
echo "Error: Python not found!"
exit 1
# Download and validate the file
if ! download_from_github "$file" "$temp_file"; then
exit 1
fi

$PYTHON your_python_script.py
# Validate the JSON file
if ! $PYTHON -m json.tool "server/config/feature_flag_configs/${file}" &> /dev/null; then
echo "Error: ${file} is not valid JSON."
if ! validate_json "$temp_file"; then
exit 1
fi

# Construct the bucket name, handling the "prod" case for production
if [[ "$environment" == "production" ]]; then
bucket_name="datcom-website-prod-resources"

# Confirmation prompt for production
read -p "Have you validated these feature flags in staging? (yes/no) " -n 1 -r
echo # (optional) move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborting deployment to production."
exit 1
fi

# Fetch staging flags from GCS
echo "Fetching staging feature flags from GCS..."
gsutil cp "gs://datcom-website-staging-resources/feature_flags.json" "staging_flags.json"

# Compare staging and production flags
echo "Comparing staging and production feature flags..."
if ! diff --color "server/config/feature_flag_configs/${file}" "staging_flags.json" &> /dev/null; then
echo "Error: Production feature flags differ from staging."
echo "Please ensure the flags are identical before deploying to production."
echo "Diffs:"
diff -C 2 --color "server/config/feature_flag_configs/${file}" "staging_flags.json"
exit 1
fi
bucket_name=$(get_bucket_name "$environment")

rm "staging_flags.json" # Clean up temporary file
else
bucket_name="datcom-website-${environment}-resources"
if [[ "$environment" == "production" ]]; then
read -p "Have you validated these feature flags in staging? (yes/no) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborting deployment to production."
exit 1
fi

if ! compare_staging_production "$temp_file"; then
exit 1
fi
fi


echo "Uploading ${file} to gs://${bucket_name}/feature_flags.json"
gsutil cp "server/config/feature_flag_configs/${file}" "gs://${bucket_name}/feature_flags.json"
gsutil cp "${temp_file}" "gs://${bucket_name}/feature_flags.json"

echo "Upload complete!"

# Prompt for Kubernetes restart
# Kubernetes restart prompt
read -p "Do you want to restart the Kubernetes deployment? (yes/no) " -n 1 -r
echo # (optional) move to a new line
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Use the appropriate project.
gcloud config set project datcom-website-${environment}

# Get the credentials for the autopush k8s cluster
gcloud container clusters get-credentials website-us-central1 --region us-central1 --project datcom-website-${environment}
restart_kubernetes_deployment "$environment" # Call the new function
fi

# Restart the deployment
kubectl rollout restart deployment website-app -n website
echo "Kubernetes deployment restarted."
fi
rm "${temp_file}"

0 comments on commit 615800a

Please sign in to comment.