Repository: aws-cloudformation/cloudformation-cli-python-plugin

Issue:
Docker build not supported on windows: module 'os' has no attribute 'geteuid'
I didn't see not supporting Windows called out anywhere in the readme

Repro steps
install:
```
pip install cloudformation-cli
pip install git+https://github.com/aws-cloudformation/aws-cloudformation-rpdk-python-plugin.git#egg=cloudformation-cli-python-plugin
```
init:
```
cfn init
Initializing new project
What's the name of your resource type?
(Organization::Service::Resource)
>> Windows::Bug::Repro
Select a language for code generation:
[1] python36
[2] python37
(enter an integer):
>> 2
Use docker for platform-independent packaging (Y/n)?
This is highly recommended unless you are experienced
with cross-platform Python packaging.
>> y
Initialized a new project in C:\temp\cfn-resource-repro

cp ..\cloudformation-cli-python-lib-0.0.1.tar.gz .\
```
```
cfn submit --dry-run
Starting Docker build. This may take several minutes if the image 'lambci/lambda:build-python3.7' needs to be pulled first.
=== Unhandled exception ===
Please report this issue to the team.
Issue tracker: https://github.com/aws-cloudformation/aws-cloudformation-rpdk/issues
Please include the log file 'rpdk.log'
```
```
[2020-06-15T15:52:09Z] DEBUG    - Logging set up successfully
[2020-06-15T15:52:09Z] DEBUG    - Running submit: Namespace(command=<function submit at 0x000001CD92DE19D0>, dry_run=True, endpoint_url=None, region=None, role_arn=None, set_default=False, subparser_name='submit', use_role=True, verbose=0, version=False)
[2020-06-15T15:52:09Z] DEBUG    - Root directory: C:\temp\cfn-resource-repro
[2020-06-15T15:52:09Z] DEBUG    - Loading project file 'C:\temp\cfn-resource-repro\.rpdk-config'
[2020-06-15T15:52:09Z] INFO     - Validating your resource specification...
[2020-06-15T15:52:09Z] DEBUG    - Resolved '#/definitions/InitechDateFormat' to 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/InitechDateFormat'
[2020-06-15T15:52:09Z] DEBUG    - Parsed 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/InitechDateFormat' to '(<BASE>, 'definitions', 'InitechDateFormat')'
[2020-06-15T15:52:09Z] DEBUG    - Edge from '(<BASE>, 'properties', 'DueDate')' to '(<BASE>, 'definitions', 'InitechDateFormat')'
[2020-06-15T15:52:09Z] DEBUG    - Resolved '#/definitions/InitechDateFormat' to 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/InitechDateFormat'
[2020-06-15T15:52:09Z] DEBUG    - Parsed 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/InitechDateFormat' to '(<BASE>, 'definitions', 'InitechDateFormat')'
[2020-06-15T15:52:09Z] DEBUG    - Edge from '(<BASE>, 'properties', 'ApprovalDate')' to '(<BASE>, 'definitions', 'InitechDateFormat')'
[2020-06-15T15:52:09Z] DEBUG    - Resolved '#/definitions/Memo' to 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/Memo'
[2020-06-15T15:52:09Z] DEBUG    - Parsed 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/Memo' to '(<BASE>, 'definitions', 'Memo')'
[2020-06-15T15:52:09Z] DEBUG    - Edge from '(<BASE>, 'properties', 'Memo')' to '(<BASE>, 'definitions', 'Memo')'
[2020-06-15T15:52:09Z] DEBUG    - Resolved '#/definitions/Memo' to 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/Memo'
[2020-06-15T15:52:09Z] DEBUG    - Parsed 'file:///C:/temp/cfn-resource-repro/windows-bug-repro.json#/definitions/Memo' to '(<BASE>, 'definitions', 'Memo')'
[2020-06-15T15:52:09Z] DEBUG    - Edge from '(<BASE>, 'properties', 'SecondCopyOfMemo')' to '(<BASE>, 'definitions', 'Memo')'
[2020-06-15T15:52:09Z] DEBUG    - Rewriting refs in '<BASE>' (file:///C:/temp/cfn-resource-repro/windows-bug-repro.json)[2020-06-15T15:52:09Z] DEBUG    -   '#/definitions/InitechDateFormat' -> '#/definitions/InitechDateFormat'
[2020-06-15T15:52:09Z] DEBUG    -   '#/definitions/InitechDateFormat' -> '#/definitions/InitechDateFormat'
[2020-06-15T15:52:09Z] DEBUG    -   '#/definitions/Memo' -> '#/definitions/Memo'
[2020-06-15T15:52:09Z] DEBUG    -   '#/definitions/Memo' -> '#/definitions/Memo'
[2020-06-15T15:52:09Z] DEBUG    - overrides.json not found. Not writing to package.
[2020-06-15T15:52:09Z] DEBUG    - Package started
[2020-06-15T15:52:09Z] DEBUG    - 'C:\temp\cfn-resource-repro\build' not found, skipping removal
Traceback (most recent call last):
  File "c:\python38\lib\site-packages\rpdk\python\codegen.py", line 192, in _remove_build_artifacts
    shutil.rmtree(deps_path)
  File "c:\python38\lib\shutil.py", line 737, in rmtree
    return _rmtree_unsafe(path, onerror)
  File "c:\python38\lib\shutil.py", line 596, in _rmtree_unsafe
    onerror(os.scandir, path, sys.exc_info())
  File "c:\python38\lib\shutil.py", line 593, in _rmtree_unsafe
    with os.scandir(path) as scandir_it:
FileNotFoundError: [WinError 3] The system cannot find the path specified: 'C:\\temp\\cfn-resource-repro\\build'
[2020-06-15T15:52:09Z] DEBUG    - Dependencies build started from 'C:\temp\cfn-resource-repro'
[2020-06-15T15:52:09Z] DEBUG    - command is 'pip install --no-cache-dir --no-color --disable-pip-version-check --upgrade --find-links /project --requirement /project/requirements.txt --target /project/build'
[2020-06-15T15:52:09Z] WARNING  - Starting Docker build. This may take several minutes if the image 'lambci/lambda:build-python3.7' needs to be pulled first.
[2020-06-15T15:52:09Z] DEBUG    - Trying paths: ['C:\\Users\\user\\.docker\\config.json', 'C:\\Users\\user\\.dockercfg']
[2020-06-15T15:52:09Z] DEBUG    - Found file at path: C:\Users\user\.docker\config.json
[2020-06-15T15:52:09Z] DEBUG    - Found 'auths' section
[2020-06-15T15:52:09Z] DEBUG    - Auth data for xxxxxxxxxxx.dkr.ecr.us-west-1.amazonaws.com is absent. Client might be using a credentials store instead.
[2020-06-15T15:52:09Z] DEBUG    - Found 'credsStore' section
[2020-06-15T15:52:09Z] DEBUG    - Unhandled exception
Traceback (most recent call last):
  File "c:\python38\lib\site-packages\rpdk\core\cli.py", line 98, in main
    args.command(args)
  File "c:\python38\lib\site-packages\rpdk\core\submit.py", line 15, in submit
    project.submit(
  File "c:\python38\lib\site-packages\rpdk\core\project.py", line 336, in submit
    self._plugin.package(self, zip_file)
  File "c:\python38\lib\site-packages\rpdk\python\codegen.py", line 180, in package
    self._build(project.root)
  File "c:\python38\lib\site-packages\rpdk\python\codegen.py", line 199, in _build
    self._docker_build(base_path)
  File "c:\python38\lib\site-packages\rpdk\python\codegen.py", line 256, in _docker_build
    user=f"{os.geteuid()}:{os.getgid()}",
AttributeError: module 'os' has no attribute 'geteuid'
```

Problem line: https://github.com/aws-cloudformation/cloudformation-cli-python-plugin/blob/20f46577f44393a2b50bb0356f825681680035e6/python/rpdk/python/codegen.py#L256

Discussion:

Patch:
diff --git a/python/rpdk/python/codegen.py b/python/rpdk/python/codegen.py
index 0ceaab8..00c653c 100644
--- a/python/rpdk/python/codegen.py
+++ b/python/rpdk/python/codegen.py
@@ -248,6 +248,17 @@ class Python36LanguagePlugin(LanguagePlugin):
             self._pip_build(base_path)
         LOG.debug("Dependencies build finished")
 
+    @staticmethod
+    def _update_pip_command():
+        return [
+            "python",
+            "-m",
+            "pip",
+            "install",
+            "--upgrade",
+            "pip",
+        ]
+
     @staticmethod
     def _make_pip_command(base_path):
         return [
@@ -270,7 +281,13 @@ class Python36LanguagePlugin(LanguagePlugin):
     @classmethod
     def _docker_build(cls, external_path):
         internal_path = PurePosixPath("/project")
-        command = " ".join(cls._make_pip_command(internal_path))
+        command = (
+            '/bin/bash -c "'
+            + " ".join(cls._update_pip_command())
+            + " && "
+            + " ".join(cls._make_pip_command(internal_path))
+            + '"'
+        )
         LOG.debug("command is '%s'", command)
 
         volumes = {str(external_path): {"bind": str(internal_path), "mode": "rw"}}
@@ -280,6 +297,27 @@ class Python36LanguagePlugin(LanguagePlugin):
             "image '%s' needs to be pulled first.",
             image,
         )
+
+        # Docker will mount the path specified in the volumes variable in the container
+        # and pip will place all the dependent packages inside the volumes/build path.
+        # codegen will need access to this directory during package()
+        try:
+            # Use root:root for euid:group when on Windows
+            # https://docs.docker.com/desktop/windows/permission-requirements/#containers-running-as-root-within-the-linux-vm
+            if os.name == "nt":
+                localuser = "root:root"
+            # Try to get current effective user ID and Group ID.
+            # Only valid on UNIX-like systems
+            else:
+                localuser = f"{os.geteuid()}:{os.getgid()}"
+        # Catch exception if geteuid failed on non-Windows system
+        # and default to root:root
+        except AttributeError:
+            localuser = "root:root"
+            LOG.warning(
+                "User ID / Group ID not found.  Using root:root for docker build"
+            )
+
         docker_client = docker.from_env()
         try:
             logs = docker_client.containers.run(
@@ -289,7 +327,7 @@ class Python36LanguagePlugin(LanguagePlugin):
                 volumes=volumes,
                 stream=True,
                 entrypoint="",
-                user=f"{os.geteuid()}:{os.getgid()}",
+                user=localuser,
             )
         except RequestsConnectionError as e:
             # it seems quite hard to reliably extract the cause from


Test patch:
diff --git a/tests/plugin/codegen_test.py b/tests/plugin/codegen_test.py
index f4857ea..0eda8e5 100644
--- a/tests/plugin/codegen_test.py
+++ b/tests/plugin/codegen_test.py
@@ -422,6 +422,84 @@ def test__build_docker(plugin):
     mock_docker.assert_called_once_with(sentinel.base_path)
 
 
+# Test _build_docker on Linux/Unix-like systems
+def test__build_docker_posix(plugin):
+    plugin._use_docker = True
+
+    patch_pip = patch.object(plugin, "_pip_build", autospec=True)
+    patch_from_env = patch("rpdk.python.codegen.docker.from_env", autospec=True)
+    patch_os_name = patch("rpdk.python.codegen.os.name", "posix")
+
+    with patch_pip as mock_pip, patch_from_env as mock_from_env:
+        mock_run = mock_from_env.return_value.containers.run
+        with patch_os_name:
+            plugin._build(sentinel.base_path)
+
+    mock_pip.assert_not_called()
+    mock_run.assert_called_once_with(
+        image=ANY,
+        command=ANY,
+        auto_remove=True,
+        volumes={str(sentinel.base_path): {"bind": "/project", "mode": "rw"}},
+        stream=True,
+        entrypoint="",
+        user=ANY,
+    )
+
+
+# Test _build_docker on Windows
+def test__build_docker_windows(plugin):
+    plugin._use_docker = True
+
+    patch_pip = patch.object(plugin, "_pip_build", autospec=True)
+    patch_from_env = patch("rpdk.python.codegen.docker.from_env", autospec=True)
+    patch_os_name = patch("rpdk.python.codegen.os.name", "nt")
+
+    with patch_pip as mock_pip, patch_from_env as mock_from_env:
+        mock_run = mock_from_env.return_value.containers.run
+        with patch_os_name:
+            plugin._build(sentinel.base_path)
+
+    mock_pip.assert_not_called()
+    mock_run.assert_called_once_with(
+        image=ANY,
+        command=ANY,
+        auto_remove=True,
+        volumes={str(sentinel.base_path): {"bind": "/project", "mode": "rw"}},
+        stream=True,
+        entrypoint="",
+        user="root:root",
+    )
+
+
+# Test _build_docker if geteuid fails
+def test__build_docker_no_euid(plugin):
+    plugin._use_docker = True
+
+    patch_pip = patch.object(plugin, "_pip_build", autospec=True)
+    patch_from_env = patch("rpdk.python.codegen.docker.from_env", autospec=True)
+    # os.geteuid does not exist on Windows so we can not autospec os
+    patch_os = patch("rpdk.python.codegen.os")
+    patch_os_name = patch("rpdk.python.codegen.os.name", "posix")
+
+    with patch_pip as mock_pip, patch_from_env as mock_from_env, patch_os as mock_patch_os:  # noqa: B950 pylint: disable=line-too-long
+        mock_run = mock_from_env.return_value.containers.run
+        mock_patch_os.geteuid.side_effect = AttributeError()
+        with patch_os_name:
+            plugin._build(sentinel.base_path)
+
+    mock_pip.assert_not_called()
+    mock_run.assert_called_once_with(
+        image=ANY,
+        command=ANY,
+        auto_remove=True,
+        volumes={str(sentinel.base_path): {"bind": "/project", "mode": "rw"}},
+        stream=True,
+        entrypoint="",
+        user="root:root",
+    )
+
+
 def test__docker_build_good_path(plugin, tmp_path):
     patch_from_env = patch("rpdk.python.codegen.docker.from_env", autospec=True)