Skip to content

Commit

Permalink
Merge pull request #93 from salsadigitalauorg/feature/group-plugin-mu…
Browse files Browse the repository at this point in the history
…tation-action-config

[DEVOPS-499] Refactored group to use the MutationActionConfig
  • Loading branch information
yusufhm authored Aug 20, 2024
2 parents 16b1dea + ea492be commit 9899ca5
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 141 deletions.
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,30 @@ gql-cli https://api.lagoon.amazeeio.cloud/graphql --print-schema \

## Run unit tests
```sh
docker-compose build test
docker-compose run --rm test units -v --requirements
docker compose build test
docker compose run --rm test units -v --requirements
```

## Creating the docs

To view the module docs in the terminal, run
```sh
# List modules
docker compose run --rm --entrypoint="" -T lint-docs bash -c \
'ansible-doc -t module lagoon.api -l'

# Specific module (group)
docker compose run --rm --entrypoint="" -T lint-docs bash -c \
'ansible-doc -t module lagoon.api.group'
```

Linting the docs
```sh
docker compose build lint-docs
docker compose run --rm lint-docs
```

Build the docs:
Build & serve the docs:
```sh
docker compose build docs
docker compose up -d docs
```

Expand Down
67 changes: 12 additions & 55 deletions api/plugins/action/group.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,15 @@
from ..module_utils.api_client import ApiClient
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native
from ansible.plugins.action import ActionBase
from . import LagoonMutationActionBase, MutationConfig, MutationActionConfig
from ..module_utils.gql import ProxyLookup


class ActionModule(ActionBase):
class ActionModule(LagoonMutationActionBase):

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._display.v("Task args: %s" % self._task.args)

lagoon = ApiClient(
self._templar.template(task_vars.get('lagoon_api_endpoint')).strip(),
self._templar.template(task_vars.get('lagoon_api_token')).strip(),
{'headers': self._task.args.get('headers', {})}
)

state = self._task.args.get('state')
name = self._task.args.get('name')
# parent = self._task.args.get('parent')

if not name:
raise AnsibleError("Missing required parameter 'name'.")

group = lagoon.group(name)

if (state == "present"):
if group['id']:
result['changed'] = False
result['group'] = group
else:
try:
result['group'] = lagoon.group_add(name)
except AnsibleError as e:
result['failed'] = True
result['msg'] = to_native(e)

elif (state == "absent"):
if not group['id']:
result['changed'] = False
else:
result['changed'] = True
try:
result['group'] = lagoon.group_remove(group['id'])
except AnsibleError as e:
result['failed'] = True
result['msg'] = to_native(e)
else:
raise AnsibleError("Invalid state '%s' operation." % (state))

return result
actionConfig = MutationActionConfig(
name="group",
add=MutationConfig(
field="addGroup",
proxyLookups=[ProxyLookup(query="groupByName")],
lookupCompareFields=["name"],
),
delete=MutationConfig(field="deleteGroup"),
)
149 changes: 76 additions & 73 deletions api/plugins/action/task_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,80 @@

class ActionModule(LagoonMutationActionBase):

actionConfig = MutationActionConfig(
name="task_definition",
# Configuration for adding a new task definition.
add=MutationConfig(
# GraphQL Mutation for adding a new task definition.
field="addAdvancedTaskDefinition",
# GraphQL Mutation for updating an existing task definition.
updateField="updateAdvancedTaskDefinition",
# Additional arguments to be allowed when calling the action
# plugin. These arguments will not be passed to the GraphQL
# mutation, but would instead be used to lookup a project by name
# in one of the proxy lookups.
inputFieldAdditionalArgs=dict(project_name=dict(type="str")),
# Aliases for the input fields - in this case used to maintain
# compatibility with the previous version of the plugin.
inputFieldArgsAliases=dict(
advancedTaskDefinitionArguments=["arguments"],
deployTokenInjection=["deploy_token_injection"],
projectKeyInjection=["project_key_injection"],
type=["task_type"],
),
# Proxy lookups to be used when looking for existing task
# definitions. A first pass is done through the lookups in the
# order they are provided, to find one that matches the input
# of the plugin. The first one matched is then used to query
# task definitions and compare them with the input, using the
# lookupCompareFields and diffCompareFields.
proxyLookups=[
# Will use 'id' if provided to lookup a task definition by id.
ProxyLookup(query="advancedTaskDefinitionById"),
# Will use 'environment' id if provided to find all task
# definitions by environment id, then filter by the fields
# in lookupCompareFields.
ProxyLookup(query="advancedTasksForEnvironment"),
# Will use 'project_name' if provided to find all task
# definitions for a project through projectByName, selecting
# the fields in selectFields recursively, then filter by the
# fields in lookupCompareFields.
# This would be similar to the following:
# query {
# projectByName(project_name: "{{ project_name }}") {
# environments {
# advancedTasks {
# id
# ...
# }
# }
# }
# }
ProxyLookup(
query="projectByName",
inputArgFields={"project_name": "name"},
selectFields=["environments", "advancedTasks"],
),
],
# These fields are used to determine whether the task definition
# already exists.
lookupCompareFields=["name"],
# These fields are used to determine whether the task definition
# needs to be updated. If any of the values of these fields are
# different between the existing task definition and the input,
# the task definition will be updated.
diffCompareFields=[
"permission",
"description",
"service",
"advancedTaskDefinitionArguments",
"deployTokenInjection",
"projectKeyInjection",
],
actionConfig = MutationActionConfig(
name="task_definition",
# Configuration for adding a new task definition.
add=MutationConfig(
# GraphQL Mutation for adding a new task definition.
field="addAdvancedTaskDefinition",
# GraphQL Mutation for updating an existing task definition.
updateField="updateAdvancedTaskDefinition",
# Additional arguments to be allowed when calling the action
# plugin. These arguments will not be passed to the GraphQL
# mutation, but would instead be used to lookup a project by name
# in one of the proxy lookups.
inputFieldAdditionalArgs=dict(project_name=dict(type="str")),
# Aliases for the input fields - in this case used to maintain
# compatibility with the previous version of the plugin.
inputFieldArgsAliases=dict(
advancedTaskDefinitionArguments=["arguments"],
deployTokenInjection=["deploy_token_injection"],
projectKeyInjection=["project_key_injection"],
type=["task_type"],
),
# Proxy lookups to be used when looking for existing task
# definitions. A first pass is done through the lookups in the
# order they are provided, to find one that matches the input
# of the plugin. The first one matched is then used to query
# task definitions and compare them with the input, using the
# lookupCompareFields and diffCompareFields.
proxyLookups=[
# Will use 'id' if provided to lookup a task definition by id.
ProxyLookup(query="advancedTaskDefinitionById"),
# Will use 'environment' id if provided to find all task
# definitions by environment id, then filter by the fields
# in lookupCompareFields.
ProxyLookup(query="advancedTasksForEnvironment"),
# Will use 'project_name' if provided to find all task
# definitions for a project through projectByName, selecting
# the fields in selectFields recursively, then filter by the
# fields in lookupCompareFields.
# This would be similar to the following:
# query {
# projectByName(project_name: "{{ project_name }}") {
# environments {
# advancedTasks {
# id
# ...
# }
# }
# }
# }
ProxyLookup(
query="projectByName",
inputArgFields={"project_name": "name"},
selectFields=["environments", "advancedTasks"],
),
# Configuration for deleting a task definition.
delete=MutationConfig(field="deleteAdvancedTaskDefinition"),
)
],
# These fields are used to determine whether the task definition
# already exists.
lookupCompareFields=["name"],
# These fields are used to determine whether the task definition
# needs to be updated. If any of the values of these fields are
# different between the existing task definition and the input,
# the task definition will be updated.
diffCompareFields=[
"permission",
"description",
"service",
"advancedTaskDefinitionArguments",
"deployTokenInjection",
"projectKeyInjection",
],
),
# Configuration for deleting a task definition.
delete=MutationConfig(
field="deleteAdvancedTaskDefinition",
proxyLookups=[ProxyLookup(query="advancedTaskDefinitionById")],
),
)
4 changes: 2 additions & 2 deletions api/plugins/module_utils/argspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def generate_argspec_from_mutation(
for fieldName, arg in mutationField.field.args.items():
argSpec[fieldName] = generate_argspec_for_input_type(arg.type, aliases)

if 'input' in argSpec:
if 'input' in argSpec and additionalArgs:
argSpec['input']['options'].update(additionalArgs)
elif additionalArgs:
argSpec.update(additionalArgs)
Expand All @@ -54,7 +54,7 @@ def generate_argspec_from_input_object_type(
field: GraphQLInputField
for fieldName, field in fields.items():
argSpec[fieldName] = generate_argspec_for_input_type(field.type, aliases)
if fieldName in aliases:
if aliases and fieldName in aliases:
argSpec[fieldName]['aliases'] = aliases[fieldName]
return argSpec

Expand Down
30 changes: 24 additions & 6 deletions api/plugins/module_utils/gql.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
)
from gql.transport.exceptions import TransportQueryError
from gql.transport.requests import RequestsHTTPTransport
from graphql import print_ast, GraphQLList, GraphQLOutputType, GraphQLUnionType
from graphql import (
print_ast,
GraphQLList,
GraphQLOutputType,
GraphQLUnionType,
)
from graphql.type.definition import (
is_enum_type,
is_interface_type,
is_list_type,
is_object_type,
is_output_type,
Expand Down Expand Up @@ -96,6 +102,11 @@ def execute_query(self, query: str, variables: Optional[Dict[str, Any]]={}) -> D
self.vvvv(f"GraphQL query result: {res}\n\n")
return res
except TransportQueryError as e:
# In some cases (groupByName), an error is returned when not found,
# whereas in others it's just an empty result (projectByName).
# Let's keep it consistent.
if 'not found' in str(e):
return e.data
self.vvvv(f"GraphQL TransportQueryError: {e}\n\n")
return {'error': e}

Expand All @@ -115,15 +126,23 @@ def execute_query_dynamic(self, *operations: DSLExecutable) -> Dict[str, Any]:
full_query = dsl_gql(*operations)
self.vvv(f"GraphQL built query: \n{print_ast(full_query)}")

opName = operations[0].selection_set.selections[0].name.value
if self.checkMode and isinstance(operations[0], DSLMutation):
self.info(f"Check mode enabled, skipping query execution. Query to execute: \n{print_ast(full_query)}")
opName = operations[0].selection_set.selections[0].name.value
if opName.startswith('delete'):
res = {opName: 'success'}
else:
res = {opName: {'id': -1 * randint(1, 1000)}}
else:
res = self.client.session.execute(full_query)
try:
res = self.client.session.execute(full_query)
except TransportQueryError as e:
# In some cases (groupByName), an error is returned when
# not found, whereas in others it's just an empty result
# (projectByName). Let's keep it consistent.
if 'not found' in str(e):
return e.data
raise e

self.vvv(f"GraphQL query result: {res}\n\n")
return res
Expand Down Expand Up @@ -249,7 +268,6 @@ def mutation_field_add_args(self,
if not isinstance(mutationField, DSLField):
raise TypeError(f"mutationField must be of type DSLField, got {type(mutationField)}.")


if not is_output_type(outputType):
raise TypeError(f"outputType must be of type GraphQLOutputType, got {type(outputType)}.")

Expand All @@ -264,7 +282,7 @@ def mutation_field_add_args(self,
set(mutationField.field.args.keys()) &
set(inputArgs.keys()))
mutationField.args(**{argsIntersect[0]: inputArgs[argsIntersect[0]]})
elif is_union_type(outputType) or is_object_type(outputType):
elif is_union_type(outputType) or is_object_type(outputType) or is_interface_type(outputType):
mutationField.args(**inputArgs)
elif is_list_type(outputType):
listObj = cast(GraphQLList, outputType)
Expand Down Expand Up @@ -447,7 +465,7 @@ def field_selector(ds: DSLSchema,
return selector
elif is_list_type(selectorType):
return field_selector(ds, selector, selectorType.of_type, selectFields)
elif is_object_type(selectorType):
elif is_object_type(selectorType) or is_interface_type(selectorType):
selectType: DSLType = getattr(ds, selectorType.name)
for f in selectFields:
if not hasattr(selectType, f):
Expand Down
Loading

0 comments on commit 9899ca5

Please sign in to comment.