Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions examples/feature_flag_management.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
- name: Feature Flag Management Example
hosts: localhost
gather_facts: false
vars:
gateway_hostname: "{{ ansible_host }}"
gateway_username: "admin"
gateway_password: "{{ gateway_admin_password }}"
gateway_validate_certs: false

tasks:
- name: Check if a feature flag exists
ansible.platform.feature_flag:
name: FEATURE_EXAMPLE_ENABLED
state: exists
gateway_hostname: "{{ gateway_hostname }}"
gateway_username: "{{ gateway_username }}"
gateway_password: "{{ gateway_password }}"
gateway_validate_certs: "{{ gateway_validate_certs }}"
register: feature_flag_info

- name: Display feature flag information
ansible.builtin.debug:
msg: |
Feature Flag: {{ feature_flag_info.name }}
Current Value: {{ feature_flag_info.value }}
Toggle Type: {{ feature_flag_info.toggle_type }}
Description: {{ feature_flag_info.description }}

- name: Enable a runtime feature flag (if it's a runtime flag)
ansible.platform.feature_flag:
name: "{{ feature_flag_info.name }}"
value: "True"
state: present
gateway_hostname: "{{ gateway_hostname }}"
gateway_username: "{{ gateway_username }}"
gateway_password: "{{ gateway_password }}"
gateway_validate_certs: "{{ gateway_validate_certs }}"
when: feature_flag_info.toggle_type == 'run-time'
register: feature_flag_update

- name: Show update result
ansible.builtin.debug:
msg: |
Feature flag update result:
Changed: {{ feature_flag_update.changed | default('N/A') }}
New Value: {{ feature_flag_update.value | default('N/A') }}
when: feature_flag_update is defined

- name: Configuration as Code Example - Ensure multiple feature flags are in desired state
ansible.platform.feature_flag:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: "{{ item.state | default('present') }}"
gateway_hostname: "{{ gateway_hostname }}"
gateway_username: "{{ gateway_username }}"
gateway_password: "{{ gateway_password }}"
gateway_validate_certs: "{{ gateway_validate_certs }}"
loop:
- name: FEATURE_EXAMPLE_ONE_ENABLED
value: "True"
- name: FEATURE_EXAMPLE_TWO_ENABLED
value: "False"
state: enforced # Ensure exact value
when: false # Set to true to run this example
register: bulk_updates

- name: Show bulk update results
ansible.builtin.debug:
msg: |
Flag: {{ item.name }}
Changed: {{ item.changed }}
Value: {{ item.value }}
loop: "{{ bulk_updates.results | default([]) }}"
when: bulk_updates is defined
...
3 changes: 2 additions & 1 deletion meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ action_groups:
- authenticator
- authenticator_map
- authenticator_user
- ca_certificate
- feature_flag
- http_port
- organization
- role_user_assignment
Expand All @@ -22,5 +24,4 @@ action_groups:
- token
- ui_plugin_route
- user
- ca_certificate
...
108 changes: 108 additions & 0 deletions plugins/module_utils/aap_feature_flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from ..module_utils.aap_object import AAPObject # noqa

__metaclass__ = type


class AAPFeatureFlag(AAPObject):
API_ENDPOINT_NAME = "feature_flags"
ITEM_TYPE = "feature_flag"

def unique_field(self):
return 'name'

def set_new_fields(self):
# Create the data that gets sent for update
# Feature flags can only be updated, not created or deleted

value = self.params.get('value')
if value is not None:
self.new_fields['value'] = value

def manage(self, auto_exit=True, fail_when_not_exists=True, **kwargs):
"""
Override the manage method for feature flags since they have special behavior:
- Feature flags cannot be created or deleted via the API
- Only runtime feature flags can be updated
- Updates are done via PATCH, not PUT
"""
self.get_existing_item()

# Feature flag must exist - they cannot be created
if self.data is None:
self.module.fail_json(msg=f"Feature flag '{self.unique_value()}' does not exist. Feature flags cannot be created via the API.")

# Store the flag data in the output
self.module.json_output.update(self.data)

# If just checking existence, return the current state
if self.exists():
if auto_exit:
self.module.exit_json(**self.module.json_output)
return

# Feature flags cannot be deleted
if self.absent():
self.module.fail_json(msg="Feature flags cannot be deleted via the API.")

# Update the feature flag if present or enforced
if self.present() or self.enforced():
self.set_new_fields()

# Check if this is a runtime feature flag
if self.data.get('toggle_type') != 'run-time':
self.module.fail_json(msg=f"Feature flag '{self.data['name']}' is an install-time flag and cannot be modified at runtime.")

# Check if runtime feature flags are enabled
runtime_enabled = self._check_runtime_feature_flags_enabled()
if not runtime_enabled:
self.module.fail_json(msg="Runtime feature flag updates are disabled. RUNTIME_FEATURE_FLAGS must be set to 'True' in settings.")

# Validate the value for boolean conditions
if self.data.get('condition') == 'boolean':
value = self.new_fields.get('value')
if value is not None and value.lower() not in ['true', 'false']:
self.module.fail_json(msg="Feature flag with boolean condition requires 'True' or 'False' value.")

# Check if update is needed
current_value = str(self.data.get('value', ''))
new_value = str(self.new_fields.get('value', ''))

if current_value != new_value:
if not self.module.check_mode:
# Perform the update via PATCH
url = self.module.build_url(f"{self.api_endpoint}/{self.data['id']}/")
response = self.module.make_request('PATCH', url, data=self.new_fields)

if response.get('status_code') not in [200, 204]:
self.module.fail_json(msg=f"Failed to update feature flag: {response}")

# Refresh the data
self.data = self.module.get_one(self.api_endpoint, name_or_id=self.unique_value())
self.module.json_output.update(self.data)

self.module.json_output['changed'] = True
else:
self.module.json_output['changed'] = False

if auto_exit:
self.module.exit_json(**self.module.json_output)

def _check_runtime_feature_flags_enabled(self):
"""
Check if runtime feature flags are enabled by querying the settings endpoint.
"""
try:
# Try to get the RUNTIME_FEATURE_FLAGS setting
settings_url = self.module.build_url('settings/')
response = self.module.make_request('GET', settings_url)

if response.get('status_code') == 200 and 'results' in response:
for setting in response['results']:
if setting.get('key') == 'RUNTIME_FEATURE_FLAGS':
return setting.get('value', '').lower() == 'true'

# Default to False if setting not found or error occurred
return False
except Exception:
# If we can't check the setting, assume it's disabled for safety
return False
181 changes: 181 additions & 0 deletions plugins/modules/feature_flag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# coding: utf-8 -*-

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function

__metaclass__ = type


DOCUMENTATION = """
---
module: feature_flag
author: Fabricio Aguiar (@fao89)
short_description: Configure feature flags in Automation Platform Gateway
description:
- Manage feature flags in the Automation Platform Gateway.
- Allows viewing and updating runtime feature flags.
- Install-time feature flags cannot be modified at runtime.
options:
name:
description:
- The name of the feature flag to manage.
- Must follow the format FEATURE_<flag-name>_ENABLED.
required: True
type: str
value:
description:
- The value to set for the feature flag.
- For boolean conditions, use 'True' or 'False'.
- Only applicable when state is 'present' or 'enforced'.
- Required when modifying feature flags.
type: str
state:
description:
- The desired state of the feature flag.
- Use 'present' to ensure the feature flag exists with the specified value.
- Use 'exists' to check if the feature flag exists without modifying it.
- Use 'absent' to remove the feature flag (not typically supported for system flags).
Comment thread
rohitthakur2590 marked this conversation as resolved.
- Use 'enforced' to ensure the feature flag value matches exactly.
choices: ["present", "absent", "exists", "enforced"]
default: "exists"
type: str

extends_documentation_fragment:
- ansible.platform.auth
"""

EXAMPLES = """
- name: Check if a feature flag exists
ansible.platform.feature_flag:
name: FEATURE_EXAMPLE_ENABLED
state: exists

- name: Enable a runtime feature flag
ansible.platform.feature_flag:
name: FEATURE_EXAMPLE_ENABLED
value: "True"
state: present

- name: Disable a runtime feature flag
ansible.platform.feature_flag:
name: FEATURE_EXAMPLE_ENABLED
value: "False"
state: present

- name: Ensure a feature flag has a specific value
ansible.platform.feature_flag:
name: FEATURE_CUSTOM_SETTING_ENABLED
value: "custom_value"
state: enforced
...
"""

RETURN = """
id:
description: The unique ID of the feature flag.
returned: always
type: int
sample: 1

name:
description: The name of the feature flag.
returned: always
type: str
sample: FEATURE_EXAMPLE_ENABLED

ui_name:
description: The display name for the feature flag.
returned: always
type: str
sample: Example Feature

condition:
description: The condition type for evaluating the flag.
returned: always
type: str
sample: boolean

value:
description: The current value of the feature flag.
returned: always
type: str
sample: True

required:
description: Whether this flag is required.
returned: always
type: bool
sample: false

support_level:
description: The support level of the feature flag.
returned: always
type: str
sample: DEVELOPER_PREVIEW

visibility:
description: Whether the flag is visible in the UI.
returned: always
type: bool
sample: true

toggle_type:
description: Whether the flag can be toggled at runtime or only install-time.
returned: always
type: str
sample: run-time

description:
description: Detailed description of the feature flag.
returned: always
type: str
sample: Enables example functionality

support_url:
description: URL for documentation about this feature.
returned: always
type: str
sample: https://docs.example.com/feature

labels:
description: List of labels associated with the feature flag.
returned: always
type: list
sample: ["experimental", "ui"]

state:
description: The current state of the feature flag (computed).
returned: always
type: bool
sample: true
"""

from ..module_utils.aap_module import AAPModule # noqa
from ..module_utils.aap_feature_flag import AAPFeatureFlag # noqa


def main():
# Define the argument specification for the module
argument_spec = dict(
name=dict(required=True, type='str'),
value=dict(type='str'),
state=dict(choices=["present", "absent", "exists", "enforced"], default="exists", type='str'),
)

# Create a module for ourselves
module = AAPModule(argument_spec=argument_spec, supports_check_mode=True)

# Validate that value is provided when state requires it
state = module.params.get('state')
value = module.params.get('value')

if state in ['present', 'enforced'] and value is None:
module.fail_json(msg="Parameter 'value' is required when state is 'present' or 'enforced'")

# Use the AAPFeatureFlag class to manage the feature flag
AAPFeatureFlag(module).manage()


if __name__ == "__main__":
main()
Loading