From 16340b3ff1e13f6c3c93de92c3f64c7b033d9d18 Mon Sep 17 00:00:00 2001 From: "Yusuf A. Hasan Miyan" Date: Fri, 19 Jul 2024 16:30:00 +0400 Subject: [PATCH] feat: add a `mutation` action plugin [DEVOPS-551] - Adds support for running dynamic mutations against the GraphQL api. --- api/plugins/action/mutation.py | 27 +++++++++++ api/plugins/module_utils/gql.py | 64 +++++++++++++++++++++++++- api/plugins/modules/mutation.py | 81 +++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 api/plugins/action/mutation.py create mode 100644 api/plugins/modules/mutation.py diff --git a/api/plugins/action/mutation.py b/api/plugins/action/mutation.py new file mode 100644 index 0000000..36633f5 --- /dev/null +++ b/api/plugins/action/mutation.py @@ -0,0 +1,27 @@ +from gql.dsl import DSLMutation +from . import LagoonActionBase + +class ActionModule(LagoonActionBase): + + def run(self, tmp=None, task_vars=None): + + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + self.createClient(task_vars) + + mutation = self._task.args.get('mutation') + input = self._task.args.get('input') + selectType = self._task.args.get('select', None) + subfields = self._task.args.get('subfields', []) + + with self.client: + mutationObj = self.client.build_dynamic_mutation( + mutation, input, selectType, subfields) + res = self.client.execute_query_dynamic(DSLMutation(mutationObj)) + result['result'] = res[mutation] + result['changed'] = True + return result diff --git a/api/plugins/module_utils/gql.py b/api/plugins/module_utils/gql.py index f60ae1a..9e98881 100644 --- a/api/plugins/module_utils/gql.py +++ b/api/plugins/module_utils/gql.py @@ -73,7 +73,13 @@ def execute_query(self, query: str, variables: Optional[Dict[str, Any]]={}) -> D self.vvvv(f"GraphQL TransportQueryError: {e}") return {'error': e} - def build_dynamic_query(self, query: str, mainType: str, args: Optional[Dict[str, Any]] = {}, fields: List[str] = [], subFieldsMap: Optional[Dict[str, List[str]]] = {}) -> DSLField: + def build_dynamic_query(self, + query: str, + mainType: str, + args: Optional[Dict[str, Any]] = {}, + fields: List[str] = [], + subFieldsMap: Optional[Dict[str, List[str]]] = {}, + ) -> DSLField: """ Dynamically build a query against the Lagoon API. @@ -132,6 +138,62 @@ def build_dynamic_query(self, query: str, mainType: str, args: Optional[Dict[str return queryObj + def build_dynamic_mutation(self, + mutation: str, + inputArgs: Optional[Dict[str, Any]] = {}, + selectType: str = '', + subfields: List[str] = [], + ) -> DSLField: + """ + Dynamically build a mutation against the Lagoon API. + + The mutation is built from the mutation name + (e.g, deployEnvironmentBranch) and a dict of input arguments + (e.g, {project: {name: "test"}, branchName: "master"} ). + + Taking the following graphql mutation as an example: + { + addFact( + input: { + environment: 243307, + name: "test_module", + value: "2.0.0", + source: "ansible_playbook:test-mutation", + description: "The test_module module version", + category: "Drupal Module Version" + } + ) { id } + } + mutation = "addFact" + inputArgs = { + environment: 243307, + name: "test_module", + value: "2.0.0", + source: "ansible_playbook:test-mutation", + description: "The test_module module version", + category: "Drupal Module Version" + } + selectType = "Fact" (since addFact returns Fact) + subfields = ["id"] + """ + + if not len(inputArgs): + raise AnsibleValidationError("Input arguments are required for mutations.") + + if selectType and not len(subfields): + raise AnsibleValidationError("Subfields are required if selectType is set.") + + # Build the main query with top-level fields if any. + mutationObj: DSLField = getattr(self.ds.Mutation, mutation) + mutationObj.args(input=inputArgs) + + if selectType: + selectTypeObj: DSLType = getattr(self.ds, selectType) + for f in subfields: + mutationObj.select(getattr(selectTypeObj, f)) + + return mutationObj + def execute_query_dynamic(self, *operations: DSLExecutable) -> Dict[str, Any]: """Executes a dynamic query with the open session. diff --git a/api/plugins/modules/mutation.py b/api/plugins/modules/mutation.py new file mode 100644 index 0000000..66a9fde --- /dev/null +++ b/api/plugins/modules/mutation.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = r''' +module: mutation +short_description: Run a mutation against the Lagoon GraphQL API. +options: + mutation: + description: + - The mutation to run, e.g, addFactsByName. + required: true + type: str + input: + description: + - Input to pass to the mutation. + required: true + type: dict + select: + description: + - The type to select from the mutation result. + type: str + default: null + subfields: + description: + - The subfields to select from the mutation result. + type: list + default: [] +''' + +EXAMPLES = r''' +- name: Delete a Fact via mutation before creating + lagoon.api.mutation: + mutation: deleteFact + input: + environment: "{{ environment_id }}" + name: lagoon_logs + +- name: Add a Fact for a project + lagoon.api.mutation: + mutation: addFact + input: + name: lagoon_logs + category: Drupal Module Version + environment: "{{ environment_id }}" + value: 2.0.0 + source: ansible_playbook:audit:module_version + description: The lagoon_logs module version + select: Fact + subfields: + - id + +- name: Delete Facts from a source via mutation before creating + lagoon.api.mutation: + mutation: deleteFactsFromSource + input: + environment: "{{ environment_id }}" + source: ansible_playbook:audit:module_version + +- name: Add multiple Facts for a project + lagoon.api.mutation: + mutation: addFactsByName + input: + project: "{{ project_name }}" + environment: "{{ environment }}" + fact: + - name: admin_toolbar + category: Drupal Module Version + environment_id: "{{ environment_id }}" + value: 2.3.0 + source: ansible_playbook:audit:module_version + description: The admin_toolbar module version + - name: panelizer + category: Drupal Module Version + environment_id: "{{ environment_id }}" + value: 4.0.0 + source: ansible_playbook:audit:module_version + description: The panelizer module version + select: Fact + subfields: + - id +'''