-
Notifications
You must be signed in to change notification settings - Fork 22
Add feature_flag module for Automation Platform Gateway #95
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). | ||
| - 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() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.