Skip to content

Commit

Permalink
[Rollout Infra] Adds the fallback flags, creates the script to upload…
Browse files Browse the repository at this point in the history
… to GCS and modifies the loading logic. (#4871)

This PR modifies the logic for reading the feature flags such that it
falls back to local files if for some reason the GCS bucket file is not
found, if there's a permission error, etc.

We also modify the logic on the 3 existing flags (autocomplete, Place
page experiment, Place page GA) to represent what is needed.

We create the local files to be used as fallback, and from which the GCS
files will get updated. Note that we enabled Autocomplete on all
environment (for the next release). But kept Place page experiment and
GA disabled for now.

Creates the script to update the GCS files from the local files.
For production, we also diff the file in the staging GCS bucket in order
to ensure it has been verified.

Lastly the script is able to trigger a Kubernetes restart.

This has all been tested end to end.
  • Loading branch information
gmechali authored Jan 24, 2025
1 parent aca728b commit 1e667ee
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 9 deletions.
119 changes: 119 additions & 0 deletions scripts/update_gcs_feature_flags.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Script to upload feature flag configurations to the website GCS bucket.
# Used for updating the feature flags used in different environments.
# Will upload the specified configuration file to the corresponding
# environment's bucket.
#
# In order to update Production, you must first update staging to have the same flags you're about to rollout to Production.
# This script can also trigger the Kubernetes pod to restart all nodes in the cluster.
#
# To Use:
# (1) Make sure you're running from root with a clean HEAD
# (2) Make sure you've signed into authenticated to gcloud using
# `gcloud auth application-default login`
# (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")

# Check if an environment is provided
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
echo "Error: Invalid environment '$environment'."
exit 1
fi

# Construct the filename (e.g., dev.json, staging.json)
file="${environment}.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
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."
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

rm "staging_flags.json" # Clean up temporary file
else
bucket_name="datcom-website-${environment}-resources"
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"

echo "Upload complete!"

# Prompt for Kubernetes restart
read -p "Do you want to restart the Kubernetes deployment? (yes/no) " -n 1 -r
echo # (optional) move to a new line
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 the deployment
kubectl rollout restart deployment website-app -n website
echo "Kubernetes deployment restarted."
fi
12 changes: 12 additions & 0 deletions server/config/feature_flag_configs/autopush.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[{
"name": "autocomplete",
"enabled": true
},
{
"name": "dev_place_experiment",
"enabled": true
},
{
"name": "dev_place_ga",
"enabled": false
}]
12 changes: 12 additions & 0 deletions server/config/feature_flag_configs/dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[{
"name": "autocomplete",
"enabled": true
},
{
"name": "dev_place_experiment",
"enabled": true
},
{
"name": "dev_place_ga",
"enabled": true
}]
12 changes: 12 additions & 0 deletions server/config/feature_flag_configs/local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[{
"name": "autocomplete",
"enabled": true
},
{
"name": "dev_place_experiment",
"enabled": true
},
{
"name": "dev_place_ga",
"enabled": false
}]
12 changes: 12 additions & 0 deletions server/config/feature_flag_configs/production.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[{
"name": "autocomplete",
"enabled": true
},
{
"name": "dev_place_experiment",
"enabled": false
},
{
"name": "dev_place_ga",
"enabled": false
}]
12 changes: 12 additions & 0 deletions server/config/feature_flag_configs/staging.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[{
"name": "autocomplete",
"enabled": true
},
{
"name": "dev_place_experiment",
"enabled": false
},
{
"name": "dev_place_ga",
"enabled": false
}]
49 changes: 43 additions & 6 deletions server/lib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,20 +392,21 @@ def get_nl_no_percapita_vars():
return nopc_vars


def get_feature_flag_bucket_name() -> str:
def get_feature_flag_bucket_name(environment: str) -> str:
"""Returns the bucket name containing the feature flags."""
env_for_bucket = os.environ.get('FLASK_ENV')
if env_for_bucket in ['local', 'integration_test', 'test', 'webdriver']:
if environment in ['integration_test', 'test', 'webdriver']:
env_for_bucket = 'autopush'
elif env_for_bucket == 'production':
elif environment == 'production':
env_for_bucket = 'prod'
else:
env_for_bucket = environment
return 'datcom-website-' + env_for_bucket + '-resources'


def load_feature_flags():
def load_feature_flags_from_gcs(environment: str):
"""Loads the feature flags into app config."""
storage_client = storage.Client()
bucket_name = get_feature_flag_bucket_name()
bucket_name = get_feature_flag_bucket_name(environment)
try:
bucket = storage_client.get_bucket(bucket_name)
except NotFound:
Expand All @@ -424,6 +425,42 @@ def load_feature_flags():
else:
logging.warning("Feature flag file not found in the bucket.")

return data


def load_fallback_feature_flags(environment: str):
"""Loads the fallback feature flags into the app config. We fallback to checked in flag configs per environment."""
environments_with_local_files = set(
['local', 'autopush', 'dev', 'staging', 'production'])
testing_environments = set(['integration_test', 'test', 'webdriver'])

if environment in testing_environments:
env_to_use = 'autopush'
elif environment in environments_with_local_files:
env_to_use = environment
else:
env_to_use = 'production'

filepath = os.path.join(get_repo_root(), "config", "feature_flag_configs",
env_to_use + ".json")

with open(filepath, 'r', encoding="utf-8") as f:
data = json.load(f)
return data


def load_feature_flags():
"""Loads the feature flags into app config."""
environment = os.environ.get('FLASK_ENV')

environment_with_gcs = set(['dev', 'autopush', 'staging', 'production'])
data = None
if environment in environment_with_gcs:
data = load_feature_flags_from_gcs(environment)

if not data:
data = load_fallback_feature_flags(environment)

# Create the dictionary using a dictionary comprehension
feature_flag_dict = {
flag["name"]: flag["enabled"]
Expand Down
11 changes: 9 additions & 2 deletions server/routes/place/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ def is_seo_experiment_enabled(place_dcid: str, category: str,
DEV_PLACE_EXPERIMENT_CONTINENT_DCIDS)


def is_dev_place_ga_enabled(request_args: MultiDict[str, str]) -> bool:
"""Determine if dev place ga should be enabled"""
return is_feature_enabled(
PLACE_PAGE_GA_FEATURE_FLAG
) and not request_args.get("disable_dev_places") == "true"


def is_dev_place_experiment_enabled(place_dcid: str, locale: str,
request_args: MultiDict[str, str]) -> bool:
"""Determine if dev place experiment should be enabled for the page"""
Expand Down Expand Up @@ -332,8 +339,8 @@ def is_dev_place_experiment_enabled(place_dcid: str, locale: str,
@bp.route('/<path:place_dcid>')
@cache.cached(query_string=True)
def place(place_dcid=None):
if is_feature_enabled(
PLACE_PAGE_GA_FEATURE_FLAG) or is_dev_place_experiment_enabled(
if is_dev_place_ga_enabled(
flask.request.args) or is_dev_place_experiment_enabled(
place_dcid, g.locale, flask.request.args):
return dev_place(place_dcid=place_dcid)
redirect_args = dict(flask.request.args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ const NlSearchBarHeaderInline = ({
useEffect(() => {
setAutoCompleteEnabled(
isFeatureEnabled(AUTOCOMPLETE_FEATURE_FLAG) ||
isAutopushEnv ||
(urlParams.has("ac_on") && urlParams.get("ac_on") == "true")
);
}, []);
Expand Down

0 comments on commit 1e667ee

Please sign in to comment.