diff --git a/docs/ref/resources/github/saltext.github.resources.github.modules.config.rst b/docs/ref/resources/github/saltext.github.resources.github.modules.config.rst new file mode 100644 index 00000000..0ba20577 --- /dev/null +++ b/docs/ref/resources/github/saltext.github.resources.github.modules.config.rst @@ -0,0 +1,5 @@ +``config`` +========== + +.. automodule:: saltext.github.resources.github.modules.config + :members: diff --git a/docs/ref/resources/github/saltext.github.resources.github.modules.rst b/docs/ref/resources/github/saltext.github.resources.github.modules.rst index 7f46f49e..722722c8 100644 --- a/docs/ref/resources/github/saltext.github.resources.github.modules.rst +++ b/docs/ref/resources/github/saltext.github.resources.github.modules.rst @@ -7,6 +7,7 @@ Execution Modules :toctree: actions + config deploy_key environment issue @@ -16,4 +17,6 @@ Execution Modules pull repo ruleset + saltutil + state test diff --git a/docs/ref/resources/github/saltext.github.resources.github.modules.saltutil.rst b/docs/ref/resources/github/saltext.github.resources.github.modules.saltutil.rst new file mode 100644 index 00000000..34aaa110 --- /dev/null +++ b/docs/ref/resources/github/saltext.github.resources.github.modules.saltutil.rst @@ -0,0 +1,5 @@ +``saltutil`` +============ + +.. automodule:: saltext.github.resources.github.modules.saltutil + :members: diff --git a/docs/ref/resources/github/saltext.github.resources.github.modules.state.rst b/docs/ref/resources/github/saltext.github.resources.github.modules.state.rst new file mode 100644 index 00000000..ffd85814 --- /dev/null +++ b/docs/ref/resources/github/saltext.github.resources.github.modules.state.rst @@ -0,0 +1,5 @@ +``state`` +========= + +.. automodule:: saltext.github.resources.github.modules.state + :members: diff --git a/docs/topics/repo_resources.md b/docs/topics/repo_resources.md index 331c3fcd..ac3bbf78 100644 --- a/docs/topics/repo_resources.md +++ b/docs/topics/repo_resources.md @@ -12,7 +12,7 @@ resource. Compared to the classic {py:mod}`github_repo ` modules, resources offer: -- direct targeting of repositories (`salt 'github:my-org/my-repo' ...`), +- direct targeting of repositories (`salt 'my-org/my-repo' ...`), including grain-based targeting across all managed repositories - per-repository grains (owner, default branch, visibility, ...) - repository-implicit execution and state modules – SLS files describe @@ -67,7 +67,7 @@ with the SRN `github:/`. ```bash # One repository -salt 'github:my-org/my-repo' test.ping +salt 'my-org/my-repo' test.ping # By resource grain salt -G 'github:default_branch:master' repo.info @@ -83,10 +83,10 @@ Jobs dispatched to a `github` resource use repository-implicit, entity-scoped execution modules instead of the classic ones: ```bash -salt 'github:my-org/my-repo' repo.update description='Managed by Salt' -salt 'github:my-org/my-repo' actions.set_secret MY_SECRET hunter2 -salt 'github:my-org/my-repo' deploy_key.list -salt 'github:my-org/my-repo' environment.get release +salt 'my-org/my-repo' repo.update description='Managed by Salt' +salt 'my-org/my-repo' actions.set_secret MY_SECRET hunter2 +salt 'my-org/my-repo' deploy_key.list +salt 'my-org/my-repo' environment.get release ``` See the {doc}`module reference ` for the @@ -124,7 +124,7 @@ Workflow permissions are restricted: ``` ```bash -salt 'github:my-org/my-repo' state.apply repo_setup +salt 'my-org/my-repo' state.apply repo_setup ``` Unlike the classic {py:func}`github_repo.present ` state, diff --git a/src/saltext/github/resources/github/modules/actions.py b/src/saltext/github/resources/github/modules/actions.py index dbcc78b1..2ec49594 100644 --- a/src/saltext/github/resources/github/modules/actions.py +++ b/src/saltext/github/resources/github/modules/actions.py @@ -33,7 +33,7 @@ def list_secrets(*, fields=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' actions.list_secrets + salt 'my-org/my-repo' actions.list_secrets fields Only include these fields in the output. A single field name @@ -57,7 +57,7 @@ def get_secret(name, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' actions.get_secret MY_SECRET + salt 'my-org/my-repo' actions.get_secret MY_SECRET name Name of the secret. @@ -80,7 +80,7 @@ def set_secret(name, value, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' actions.set_secret MY_SECRET hunter2 + salt 'my-org/my-repo' actions.set_secret MY_SECRET hunter2 name Name of the secret. @@ -105,7 +105,7 @@ def remove_secret(name, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' actions.remove_secret MY_SECRET + salt 'my-org/my-repo' actions.remove_secret MY_SECRET name Name of the secret. @@ -129,7 +129,7 @@ def get_workflow_permissions(*, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' actions.get_workflow_permissions + salt 'my-org/my-repo' actions.get_workflow_permissions profile Profile configuration name to use. Defaults to the resource @@ -150,7 +150,7 @@ def set_workflow_permissions( .. code-block:: bash - salt 'github:my-org/my-repo' actions.set_workflow_permissions \\ + salt 'my-org/my-repo' actions.set_workflow_permissions \\ default_workflow_permissions=read can_approve_pull_request_reviews=true default_workflow_permissions diff --git a/src/saltext/github/resources/github/modules/config.py b/src/saltext/github/resources/github/modules/config.py new file mode 100644 index 00000000..183a968f --- /dev/null +++ b/src/saltext/github/resources/github/modules/config.py @@ -0,0 +1,69 @@ +""" +Look up configuration values from the managing minion. + +.. versionadded:: 1.1.0 + +Repositories have no minion configuration of their own — GitHub +profiles and related settings live on the managing minion. The stock +:py:mod:`state ` execution module (re-exported for +this resource type) relies on ``config.get``/``config.option``, so this +override provides them: it delegates to the managing minion's loader +when the ``__minion__`` escape hatch is packed (state-engine context) +and falls back to the managing minion's opts otherwise (the minion's +dispatch-context per-type loaders do not pack ``__minion__``). +""" + +import logging + +import salt.utils.data + +log = logging.getLogger(__name__) + + +def _minion(): + try: + return __minion__ + except NameError: + return None + + +def get(key, default="", delimiter=":", omit_opts=False, **kwargs): + """ + Look up a config value on the managing minion. See + :py:func:`config.get ` for details. + + CLI Example: + + .. code-block:: bash + + salt 'my-org/my-repo' config.get state_verbose + """ + minion = _minion() + if minion is not None: + return minion["config.get"]( + key, default=default, delimiter=delimiter, omit_opts=omit_opts, **kwargs + ) + if omit_opts: + return default + return salt.utils.data.traverse_dict_and_list(__opts__, key, default, delimiter) + + +def option(value, default="", omit_opts=False, omit_all=False, **kwargs): + """ + Return the value of a specific config option on the managing minion. + See :py:func:`config.option ` for details. + + CLI Example: + + .. code-block:: bash + + salt 'my-org/my-repo' config.option state_queue + """ + minion = _minion() + if minion is not None: + return minion["config.option"]( + value, default=default, omit_opts=omit_opts, omit_all=omit_all, **kwargs + ) + if not (omit_opts or omit_all) and value in __opts__: + return __opts__[value] + return default diff --git a/src/saltext/github/resources/github/modules/deploy_key.py b/src/saltext/github/resources/github/modules/deploy_key.py index 1fdf9ba7..9fc98be9 100644 --- a/src/saltext/github/resources/github/modules/deploy_key.py +++ b/src/saltext/github/resources/github/modules/deploy_key.py @@ -32,7 +32,7 @@ def list_(*, fields=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' deploy_key.list + salt 'my-org/my-repo' deploy_key.list fields Only include these fields in the output. A single field name @@ -54,7 +54,7 @@ def get(key_id, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' deploy_key.get 12345 + salt 'my-org/my-repo' deploy_key.get 12345 key_id ID of the deploy key. @@ -79,7 +79,7 @@ def add(title, key, *, read_only=False, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' deploy_key.add autorelease 'ssh-ed25519 AAAA...' read_only=false + salt 'my-org/my-repo' deploy_key.add autorelease 'ssh-ed25519 AAAA...' read_only=false title Display title of the deploy key. @@ -108,7 +108,7 @@ def remove(key_id, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' deploy_key.remove 12345 + salt 'my-org/my-repo' deploy_key.remove 12345 key_id ID of the deploy key. diff --git a/src/saltext/github/resources/github/modules/issue.py b/src/saltext/github/resources/github/modules/issue.py index 1e623d9d..12d03646 100644 --- a/src/saltext/github/resources/github/modules/issue.py +++ b/src/saltext/github/resources/github/modules/issue.py @@ -47,7 +47,7 @@ def list_( .. code-block:: bash - salt 'github:my-org/my-repo' issue.list + salt 'my-org/my-repo' issue.list milestone Number of a GitHub milestone, or a string of either ``*`` or ``none``. @@ -119,7 +119,7 @@ def get(number, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' issue.get 514 + salt 'my-org/my-repo' issue.get 514 number Issue number to retrieve. @@ -140,7 +140,7 @@ def get_comments(number, *, since=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' issue.get_comments 514 + salt 'my-org/my-repo' issue.get_comments 514 number Issue number for which to retrieve comments. diff --git a/src/saltext/github/resources/github/modules/label.py b/src/saltext/github/resources/github/modules/label.py index 690d2f6a..109356e7 100644 --- a/src/saltext/github/resources/github/modules/label.py +++ b/src/saltext/github/resources/github/modules/label.py @@ -33,7 +33,7 @@ def list_(*, fields=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' label.list + salt 'my-org/my-repo' label.list fields Only include these fields in the output. A single field name @@ -56,7 +56,7 @@ def get(name, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' label.get bug + salt 'my-org/my-repo' label.get bug name Name of the label. @@ -78,7 +78,7 @@ def add(name, *, color=None, description=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' label.add release color=00ff00 + salt 'my-org/my-repo' label.add release color=00ff00 name Name of the label. @@ -109,7 +109,7 @@ def edit(name, *, new_name=None, color=None, description=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' label.edit release color=00ff00 + salt 'my-org/my-repo' label.edit release color=00ff00 name Current name of the label. @@ -147,7 +147,7 @@ def remove(name, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' label.remove release + salt 'my-org/my-repo' label.remove release name Name of the label. diff --git a/src/saltext/github/resources/github/modules/milestone.py b/src/saltext/github/resources/github/modules/milestone.py index 3db2d673..fa575dcc 100644 --- a/src/saltext/github/resources/github/modules/milestone.py +++ b/src/saltext/github/resources/github/modules/milestone.py @@ -35,7 +35,7 @@ def list_( .. code-block:: bash - salt 'github:my-org/my-repo' milestone.list + salt 'my-org/my-repo' milestone.list state Filter milestones by their state. Either ``open``, ``closed``, or ``all``. @@ -81,8 +81,8 @@ def get(number=None, *, name=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' milestone.get 72 - salt 'github:my-org/my-repo' milestone.get name=my_milestone + salt 'my-org/my-repo' milestone.get 72 + salt 'my-org/my-repo' milestone.get name=my_milestone number Milestone number to retrieve. If provided, overrides ``name``. diff --git a/src/saltext/github/resources/github/modules/pages.py b/src/saltext/github/resources/github/modules/pages.py index 02dd1356..72c8daef 100644 --- a/src/saltext/github/resources/github/modules/pages.py +++ b/src/saltext/github/resources/github/modules/pages.py @@ -30,7 +30,7 @@ def get(*, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' pages.get + salt 'my-org/my-repo' pages.get profile Profile configuration name to use. Defaults to the resource @@ -49,7 +49,7 @@ def setup(*, build_type=None, source_branch=None, source_path=None, profile=None .. code-block:: bash - salt 'github:my-org/my-repo' pages.setup build_type=workflow + salt 'my-org/my-repo' pages.setup build_type=workflow build_type Process by which the site is built, either ``legacy`` @@ -93,7 +93,7 @@ def update( .. code-block:: bash - salt 'github:my-org/my-repo' pages.update build_type=workflow + salt 'my-org/my-repo' pages.update build_type=workflow build_type Process by which the site is built, either ``legacy`` @@ -138,7 +138,7 @@ def remove(*, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' pages.remove + salt 'my-org/my-repo' pages.remove profile Profile configuration name to use. Defaults to the resource diff --git a/src/saltext/github/resources/github/modules/pull.py b/src/saltext/github/resources/github/modules/pull.py index 9250d81a..6e85ed17 100644 --- a/src/saltext/github/resources/github/modules/pull.py +++ b/src/saltext/github/resources/github/modules/pull.py @@ -43,7 +43,7 @@ def list_( .. code-block:: bash - salt 'github:my-org/my-repo' pull.list + salt 'my-org/my-repo' pull.list state Filter returned pull requests by their state. Can be either diff --git a/src/saltext/github/resources/github/modules/repo.py b/src/saltext/github/resources/github/modules/repo.py index 13e5db1c..b8bf7463 100644 --- a/src/saltext/github/resources/github/modules/repo.py +++ b/src/saltext/github/resources/github/modules/repo.py @@ -34,7 +34,7 @@ def ident(): .. code-block:: bash - salt 'github:my-org/my-repo' repo.ident + salt 'my-org/my-repo' repo.ident """ owner, _, repo_name = __resource__["id"].partition("/") return {"id": __resource__["id"], "owner": owner, "repo": repo_name} @@ -48,7 +48,7 @@ def info(*, ignore_cache=False, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' repo.info + salt 'my-org/my-repo' repo.info ignore_cache Bypass the use of cached repository information. @@ -70,7 +70,7 @@ def get_teams(*, fields=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' repo.get_teams + salt 'my-org/my-repo' repo.get_teams fields Only include these fields in the output. A single field name @@ -104,7 +104,7 @@ def update( .. code-block:: bash - salt 'github:my-org/my-repo' repo.update description='Managed by Salt' + salt 'my-org/my-repo' repo.update description='Managed by Salt' description Repository description. diff --git a/src/saltext/github/resources/github/modules/ruleset.py b/src/saltext/github/resources/github/modules/ruleset.py index ae2c5aee..07ebeb5e 100644 --- a/src/saltext/github/resources/github/modules/ruleset.py +++ b/src/saltext/github/resources/github/modules/ruleset.py @@ -32,7 +32,7 @@ def list_(*, per_page=None, fields=None, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' ruleset.list + salt 'my-org/my-repo' ruleset.list per_page Pagination size, maximum 100. @@ -60,7 +60,7 @@ def get(ruleset_id, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' ruleset.get 1234 + salt 'my-org/my-repo' ruleset.get 1234 ruleset_id ID of the ruleset. @@ -82,7 +82,7 @@ def add(ruleset_params, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' ruleset.add '{name: test, enforcement: active}' + salt 'my-org/my-repo' ruleset.add '{name: test, enforcement: active}' ruleset_params Ruleset definition. Must contain at least ``name`` and ``enforcement``. @@ -104,7 +104,7 @@ def update(ruleset_id, ruleset_params, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' ruleset.update 1234 '{enforcement: disabled}' + salt 'my-org/my-repo' ruleset.update 1234 '{enforcement: disabled}' ruleset_id ID of the ruleset. @@ -130,7 +130,7 @@ def remove(ruleset_id, *, profile=None): .. code-block:: bash - salt 'github:my-org/my-repo' ruleset.remove 1234 + salt 'my-org/my-repo' ruleset.remove 1234 ruleset_id ID of the ruleset. diff --git a/src/saltext/github/resources/github/modules/saltutil.py b/src/saltext/github/resources/github/modules/saltutil.py new file mode 100644 index 00000000..53be68fb --- /dev/null +++ b/src/saltext/github/resources/github/modules/saltutil.py @@ -0,0 +1,24 @@ +""" +Inspect running jobs on the managing minion. + +.. versionadded:: 1.1.0 + +The stock :py:mod:`state ` execution module +(re-exported for this resource type) performs conflict detection via +``saltutil.is_running``. State runs for resources execute inline on the +managing minion, so its job table is the correct one to consult. Both +functions only need ``__opts__``, so the stock implementations are +re-exported unchanged. +""" + +import logging + +import salt.modules.saltutil as _stock +from salt.utils.functools import namespaced_function + +log = logging.getLogger(__name__) + +running = namespaced_function(_stock.running, globals()) +is_running = namespaced_function(_stock.is_running, globals()) + +__load__ = ["running", "is_running"] diff --git a/src/saltext/github/resources/github/modules/state.py b/src/saltext/github/resources/github/modules/state.py new file mode 100644 index 00000000..c1257e4b --- /dev/null +++ b/src/saltext/github/resources/github/modules/state.py @@ -0,0 +1,116 @@ +""" +Apply states to the targeted repository. + +.. versionadded:: 1.1.0 + +Salt's per-resource execution loader is deny-by-default, so a resource +type must provide the ``state`` slot itself for ``state.apply`` and +friends to be dispatchable to its resources. This module re-exports the +stock :py:mod:`state ` execution module into the +per-type loader via ``namespaced_function`` (the pattern recommended in +Salt's resource state-authoring docs). The state engine then compiles +and runs on the managing minion while every module call inside the +rendered states dispatches through the per-resource loader — and thus +through this extension's ``resources/github/states/`` overrides. + +Unlike the ``ssh`` resource type, no custom transport is needed: the +GitHub API is reachable from the managing minion, so the stock +implementations work unchanged. Only a few helpers that assume +managing-minion dunders (``__pillar__``, grains-backed state disabling) +are overridden below. +""" + +import inspect +import logging + +import salt.modules.state as _stock +from salt.utils.functools import namespaced_function + +log = logging.getLogger(__name__) + +__func_alias__ = {"apply_": "apply"} + +# Only these functions are exposed as ``state.*`` for the github +# resource type. Everything else from the stock module is namespaced +# below purely so internal helper calls resolve against this module's +# (loader-packed) globals, but stays unexposed. Notably absent: +# queue/pause plumbing, ``disable``/``enable`` (grains-backed), +# ``pkg`` (salt-thin transport) and ``event``. +__load__ = [ + "apply_", + "highstate", + "id_exists", + "running", + "show_highstate", + "show_low_sls", + "show_lowstate", + "show_sls", + "show_states", + "show_top", + "single", + "sls", + "sls_exists", + "sls_id", + "test", + "top", +] + + +def _namespace_stock(): + """ + Re-export every function defined in the stock module under this + module's globals. The public entry points call module-level helpers + (``_check_queue``, ``_get_test_value``, …) which access ``__salt__`` + and friends — those helpers must be namespaced too, or they would + resolve dunders against the raw (unpacked) ``salt.modules.state`` + module and fail with a ``NameError``. + """ + for fname, func in list(vars(_stock).items()): + if inspect.isfunction(func) and func.__module__ == _stock.__name__: + globals()[fname] = namespaced_function(func, globals()) + + +_namespace_stock() + + +# --------------------------------------------------------------------------- +# Resource-context adjustments. Defined after the re-export so they +# shadow the stock implementations. +# --------------------------------------------------------------------------- + + +def _get_initial_pillar(opts): + """ + Use the managing minion's in-memory pillar when it matches the + requested pillarenv. + + The stock implementation reads ``__pillar__``, which is not packed + into per-resource loaders. The managing minion's pillar contains the + resource declarations anyway, and reusing it avoids recompiling + pillar once per targeted repository. + """ + pillar = __opts__.get("pillar") + if pillar and opts.get("pillarenv") == __opts__.get("pillarenv"): + try: + return pillar.value() + except AttributeError: + return pillar + return None + + +def _get_pillar_errors(kwargs, pillar=None): + """ + Stock implementation minus the in-memory ``__pillar__`` fallback, + which is not packed into per-resource loaders. + """ + if kwargs.get("force"): + return None + return (pillar or {}).get("_errors") + + +def _disabled(funs): # pylint: disable=unused-argument + """ + ``state.disable``/``state.enable`` (grains-backed) are not exposed + for the github resource type, so state runs can never be disabled. + """ + return [] diff --git a/src/saltext/github/resources/github/modules/test.py b/src/saltext/github/resources/github/modules/test.py index 7aa2b267..690c4b4b 100644 --- a/src/saltext/github/resources/github/modules/test.py +++ b/src/saltext/github/resources/github/modules/test.py @@ -24,6 +24,6 @@ def ping(): .. code-block:: bash - salt 'github:my-org/my-repo' test.ping + salt 'my-org/my-repo' test.ping """ return __resource_funcs__["github.ping"]() diff --git a/tests/functional/resources/modules/test_state.py b/tests/functional/resources/modules/test_state.py new file mode 100644 index 00000000..b9982bff --- /dev/null +++ b/tests/functional/resources/modules/test_state.py @@ -0,0 +1,123 @@ +""" +Tests for the ``state`` execution module override, which makes +``state.apply``/``state.single`` & friends dispatchable to ``github`` +resources (the per-resource loader is deny-by-default, so without the +override Salt reports them as unsupported for the resource type). +""" + +import pytest + +EXPOSED = ( + "state.apply", + "state.highstate", + "state.show_sls", + "state.single", + "state.sls", + "state.sls_id", +) + +UNEXPOSED = ( + "state.clear_cache", + "state.disable", + "state.enable", + "state.event", + "state.pkg", +) + + +@pytest.fixture(params=(False, True)) +def testmode(request): + return request.param + + +def test_exposed_functions(res_modules): + for fun in EXPOSED: + assert fun in res_modules + + +def test_unexposed_functions(res_modules): + """ + Only the state-application surface is re-exported; minion-local + plumbing must not be reachable via the resource loader. + """ + # Ensure the module is loaded before checking for missing functions + assert "state.single" in res_modules + for fun in UNEXPOSED: + assert fun not in res_modules + + +def test_single_override_state(res_modules, fake_org, for_resource, testmode): + """ + ``state.single`` runs this extension's state overrides against the + targeted repository. + """ + with for_resource("testorg/repo-a"): + ret = res_modules["state.single"]( + "label.present", name="release", color="#00FF00", test=testmode + ) + assert isinstance(ret, dict) + chunk = ret["label_|-release_|-release_|-present"] + assert chunk["result"] is not False + assert (chunk["result"] is None) is testmode + assert chunk["changes"] == {"created": "release"} + labels = fake_org.get_repo("repo-a").labels + assert ("release" in labels) is not testmode + + +@pytest.mark.usefixtures("testmode") +def test_single_override_state_idempotent(res_modules, fake_github, for_resource): + before = len(fake_github.mutations()) + with for_resource("testorg/repo-a"): + ret = res_modules["state.single"]("label.present", name="bug", color="#D73A4A") + chunk = ret["label_|-bug_|-bug_|-present"] + assert chunk["result"] is True + assert not chunk["changes"] + assert len(fake_github.mutations()) == before + + +def test_single_stock_state(res_modules, for_resource): + """ + Stock state modules are available unchanged in the resource context. + """ + with for_resource("testorg/repo-a"): + ret = res_modules["state.single"]("test.succeed_without_changes", name="sanity") + chunk = ret["test_|-sanity_|-sanity_|-succeed_without_changes"] + assert chunk["result"] is True + + +def test_sls(res_modules, fake_org, state_tree, for_resource, testmode): + """ + ``state.sls`` compiles from the managing minion's fileserver and runs + the chunks through the per-resource loader. + """ + sls_contents = """ + managed label: + label.present: + - name: from-sls + - color: "#0000FF" + """ + with pytest.helpers.temp_file("gh_label.sls", sls_contents, state_tree): + with for_resource("testorg/repo-a"): + ret = res_modules["state.sls"]("gh_label", test=testmode) + assert isinstance(ret, dict) + chunk = ret["label_|-managed label_|-from-sls_|-present"] + assert chunk["result"] is not False + assert (chunk["result"] is None) is testmode + assert chunk["changes"] == {"created": "from-sls"} + labels = fake_org.get_repo("repo-a").labels + assert ("from-sls" in labels) is not testmode + + +def test_show_sls(res_modules, state_tree, for_resource): + sls_contents = """ + managed label: + label.present: + - name: from-sls + - color: "#0000FF" + """ + with pytest.helpers.temp_file("gh_label.sls", sls_contents, state_tree): + with for_resource("testorg/repo-a"): + ret = res_modules["state.show_sls"]("gh_label") + assert isinstance(ret, dict) + assert "managed label" in ret + assert "label" in ret["managed label"]