Skip to content
Snippets Groups Projects
Commit 7a68a756 authored by Georges Racinet's avatar Georges Racinet
Browse files

Feature flags subsystem

The first usage will be `gitaly-simplify-find-local-branches-response`,
which is needed for GitLab 15.4 (with update of protocol).

For now the definition and default values are harcoded, but we
will probably later on synchronize them from YaML files from
the Rails application (ideally with code generation to avoid
dependency and parsing at the time of startup).

We provide full coverage in unit tests right away, so that it
will keep on being covered even when no feature flag is in use.

Closes #121
parent 23b7ef2c
No related branches found
No related tags found
Loading
# Copyright 2023 Georges Racinet <georges.racinet@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
#
# SPDX-License-Identifier: GPL-2.0-or-later
import grpc
import os
# for now, we can simply list all the feature flags that we support
# later on, we'll probably want an automatic extraction: it's not
# hard if we require an either from
# Golang definition or from the Rails app repository. The latter
# is easier, because of YaML format. We could even copy them over.
#
# Note: we see no problem within this Python code to use dash
# as separator. No need to go back and forth between dashes and underscores
# as the Golang implementation does. In Ruby case, the underscores have the
# advantage of being usable as symbols without any quotes.
FEATURE_FLAGS = { # name -> default value
}
GRPC_PREFIX = 'gitaly-feature-'
ALL_ENABLED = (
os.environ.get('GITALY_TESTING_ENABLE_ALL_FEATURE_FLAGS') == 'true'
)
class UndefinedError(LookupError):
"""Exception used to qualify query of undefined feature flag.
This is about HGitaly code querying the value of undefined flag, not
about incoming requests bearing unknown feature flags.
In most cases, that means the feature flag has to be defined.
"""
def is_enabled(context: grpc.ServicerContext, name: str) -> bool:
"""Return whether a given feature flag is enabled
This is meant for HGitaly servicer code.
:raises UndefinedError: if the feature flag is not defined. This strict
policy is made possible by the fact that this method is not used to
validate incoming feature flags (we expect to ignore many of them),
rather as a HGitaly service method implementation expecting a
feature flag to exist, given in litteral form. This will break the
service tests if a feature flag has just been removed and the caller has
not (yet). With our 100% coverage policy, this is not a hazard for
production: all calls to `is_enabled` are covered.
"""
if ALL_ENABLED:
if name not in FEATURE_FLAGS:
raise UndefinedError(name)
return True
md_key = GRPC_PREFIX + name
md = dict(context.invocation_metadata())
val = md.get(md_key)
if val is None:
try:
return FEATURE_FLAGS[name]
except KeyError:
raise UndefinedError(name)
return val == 'true'
def as_grpc_metadata(flag_values):
if flag_values is None:
return None
return [((GRPC_PREFIX + k).encode('ascii'), b'true' if v else b'false')
for k, v in flag_values]
# Copyright 2023 Georges Racinet <georges.racinet@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
#
# SPDX-License-Identifier: GPL-2.0-or-later
# TODO bring full coverage directly from here, because there might
# be points in time where it won't be used at all by gRPC methods
import pytest
from ..testing.context import FakeServicerContext
from .. import feature
class FakeContext(FakeServicerContext):
def __init__(self, invocation_metadata):
self._invocation_metadata = invocation_metadata
def invocation_metadata(self):
return self._invocation_metadata
@pytest.fixture
def feature_flags(monkeypatch):
monkeypatch.setattr(feature, 'FEATURE_FLAGS',
{'default-disabled': False,
'default-enabled': True,
})
# in case we do a full tests run with the all-enabling environment
# variable (can be useful to detect flags that we should implement)
monkeypatch.setattr(feature, 'ALL_ENABLED', False)
yield monkeypatch
def test_is_enabled_defaults(feature_flags):
context = FakeContext(())
assert feature.is_enabled(context, 'default-enabled')
assert not feature.is_enabled(context, 'default-disabled')
with pytest.raises(feature.UndefinedError) as exc_info:
feature.is_enabled(context, 'not-defined')
assert exc_info.value.args == ('not-defined', )
def test_is_enabled_all_enabled(feature_flags):
context = FakeContext(())
feature_flags.setattr(feature, 'ALL_ENABLED', True)
assert feature.is_enabled(context, 'default-disabled')
# still raises when HGitaly wants to use an undefined flag
with pytest.raises(feature.UndefinedError) as exc_info:
feature.is_enabled(context, 'not-defined')
assert exc_info.value.args == ('not-defined', )
def test_is_enabled_context(feature_flags):
context = FakeContext((
('gitaly-feature-default-disabled', 'true'),
))
assert feature.is_enabled(context, 'default-disabled')
def test_as_grpc_metadata():
assert feature.as_grpc_metadata(None) is None
assert feature.as_grpc_metadata((
('my-flag', True),
('their-flag', False),
)) == [
(b'gitaly-feature-my-flag', b'true'),
(b'gitaly-feature-their-flag', b'false'),
]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment