Repository: mosaicml/composer

Issue:
Don't upload certain hparams to experiment tracking services
Since WandB is uploading all training hparams, it's also uploading our key and secret for cloud blob storage. Probably not what we want


Discussion:
Also see https://github.com/mosaicml/yahp/issues/86

Resolving patch:
diff --git a/composer/utils/object_store.py b/composer/utils/object_store.py
--- a/composer/utils/object_store.py
+++ b/composer/utils/object_store.py
@@ -1,6 +1,7 @@
 # Copyright 2021 MosaicML. All Rights Reserved.
 
 import dataclasses
+import os
 import sys
 import textwrap
 from typing import Any, Dict, Iterator, Optional, Union
@@ -21,14 +22,42 @@ class ObjectStoreProviderHparams(hp.Hparams):
             to use S3, specify 's3' here.
 
         container (str): The name of the container (i.e. bucket) to use.
-        key (str, optional): API key or username to use to connect to the provider. (default: ``None``)
-        secret (str, optional): API secret to use to connect to the provider.
+        key_environ (str, optional): The name of an environment variable containing the API key or username
+            to use to connect to the provider. For security reasons, composer requires that the key be specified
+            via an environment variable.
             
-            .. warning::
-                
-                For security. do NOT hardcode secrets in code. Instead, please specify via CLI arguments, or even better,
-                environment variables.
+            For example, if your key is an environment variable called ``OBJECT_STORE_KEY``, then you should set this
+            parameter equal to ``OBJECT_STORE_KEY``. Composer will read the key like this:
+            
+            .. code-block:: python
+
+                import os
+
+                params = ObjectStoreProviderHparams(key_environ="OBJECT_STORE_KEY")
+
+                key = None if params.key_environ is None else os.environ[params.key_environ]
         
+            If no key is required, then set this field to ``None``. (default: ``None``)
+
+        secret_environ (str, optional): The name of an environment variable containing the API secret  or password
+            to use for the provider. For security reasons, composer requires that the secret be specified
+            via an environment variable.
+            
+            For example, if your secret is an environment variable called ``OBJECT_STORE_SECRET``, then you should set
+            this parameter equal to ``OBJECT_STORE_SECRET``. Composer will read the key like this:
+            
+            Composer will access this environment variable like so:
+            
+            .. code-block:: python
+
+                import os
+
+                params = ObjectStoreProviderHparams(secret_environ="OBJECT_STORE_SECRET")
+
+                secret = None if params.secret_environ is None else os.environ[params.secret_environ]
+        
+            If no secret is required, then set this field to ``None``. (default: ``None``)
+
         region (str, optional): Cloud region to use for the cloud provider.
             Most providers do not require the region to be specified. (default: ``None``)
         host (str, optional): Override the hostname for the cloud provider. (default: ``None``)
@@ -39,11 +68,12 @@ class ObjectStoreProviderHparams(hp.Hparams):
 
     provider: str = hp.required("Cloud provider to use.")
     container: str = hp.required("The name of the container (i.e. bucket) to use.")
-    key: Optional[str] = hp.optional("API key or username to use to connect to the provider.", default=None)
-    secret: Optional[str] = hp.optional(textwrap.dedent(
-        """API secret to use to connect to the provider. For security. do NOT hardcode the secret in the YAML.
-Instead, please specify via CLI arguments, or even better, environment variables."""),
-                                        default=None)
+    key_environ: Optional[str] = hp.optional(textwrap.dedent("""The name of an environment variable containing
+        an API key or username to use to connect to the provider."""),
+                                             default=None)
+    secret_environ: Optional[str] = hp.optional(textwrap.dedent("""The name of an environment variable containing
+        an API secret or password to use to connect to the provider."""),
+                                                default=None)
     region: Optional[str] = hp.optional("Cloud region to use", default=None)
     host: Optional[str] = hp.optional("Override hostname for connections", default=None)
     port: Optional[int] = hp.optional("Override port for connections", default=None)
@@ -52,10 +82,12 @@ class ObjectStoreProviderHparams(hp.Hparams):
 
     def initialize_object(self):
         init_kwargs = {}
-        for key in ("key", "secret", "host", "port", "region"):
+        for key in ("host", "port", "region"):
             kwarg = getattr(self, key)
             if getattr(self, key) is not None:
                 init_kwargs[key] = kwarg
+        init_kwargs["key"] = None if self.key_environ is None else os.environ[self.key_environ]
+        init_kwargs["secret"] = None if self.secret_environ is None else os.environ[self.secret_environ]
         init_kwargs.update(self.extra_init_kwargs)
         return ObjectStoreProvider(
             provider=self.provider,


Test patch:
diff --git a/tests/callbacks/test_run_directory_uploader.py b/tests/callbacks/test_run_directory_uploader.py
--- a/tests/callbacks/test_run_directory_uploader.py
+++ b/tests/callbacks/test_run_directory_uploader.py
@@ -14,7 +14,8 @@
 
 @pytest.mark.parametrize("use_procs", [False, True])
 @pytest.mark.timeout(15)
-def test_run_directory_uploader(tmpdir: pathlib.Path, use_procs: bool, dummy_state: State, dummy_logger: Logger):
+def test_run_directory_uploader(tmpdir: pathlib.Path, use_procs: bool, dummy_state: State, dummy_logger: Logger,
+                                monkeypatch: pytest.MonkeyPatch):
     try:
         import libcloud
         del libcloud
@@ -23,10 +24,11 @@ def test_run_directory_uploader(tmpdir: pathlib.Path, use_procs: bool, dummy_sta
     remote_dir = str(tmpdir / "run_directory_copy")
 
     os.makedirs(remote_dir, exist_ok=True)
+    monkeypatch.setenv("RUN_DIRECTORY_UPLOADER_KEY", remote_dir)  # for the local option, the key is the path
     hparams = RunDirectoryUploaderHparams(
         provider='local',
         upload_every_n_batches=1,
-        key=remote_dir,  # for the local option, the key is the path
+        key_environ="RUN_DIRECTORY_UPLOADER_KEY",
         container=".",
         num_concurrent_uploads=1,
         use_procs=use_procs,
diff --git a/tests/test_object_store.py b/tests/test_object_store.py
--- a/tests/test_object_store.py
+++ b/tests/test_object_store.py
@@ -8,7 +8,7 @@
 from composer.utils.object_store import ObjectStoreProviderHparams
 
 
-def test_object_store(tmpdir: pathlib.Path):
+def test_object_store(tmpdir: pathlib.Path, monkeypatch: pytest.MonkeyPatch):
     try:
         import libcloud
         del libcloud
@@ -18,9 +18,10 @@ def test_object_store(tmpdir: pathlib.Path):
     os.makedirs(remote_dir)
     local_dir = str(tmpdir / "local_dir")
     os.makedirs(local_dir)
+    monkeypatch.setenv("OBJECT_STORE_KEY", remote_dir)  # for the local option, the key is the path
     provider_hparams = ObjectStoreProviderHparams(
         provider='local',
-        key=remote_dir,  # for the local option, the key is the path
+        key_environ="OBJECT_STORE_KEY",
         container=".",
     )
     provider = provider_hparams.initialize_object()
diff --git a/tests/trainer/test_checkpoint.py b/tests/trainer/test_checkpoint.py
--- a/tests/trainer/test_checkpoint.py
+++ b/tests/trainer/test_checkpoint.py
@@ -436,7 +436,7 @@ def test_checkpoint_load_uri(tmpdir: pathlib.Path):
         assert f.readline().startswith("<!doctype html>")
 
 
-def test_checkpoint_load_object_uri(tmpdir: pathlib.Path):
+def test_checkpoint_load_object_uri(tmpdir: pathlib.Path, monkeypatch: pytest.MonkeyPatch):
     try:
         import libcloud
         del libcloud
@@ -445,9 +445,10 @@ def test_checkpoint_load_object_uri(tmpdir: pathlib.Path):
 
     remote_dir = tmpdir / "remote_dir"
     os.makedirs(remote_dir)
+    monkeypatch.setenv("OBJECT_STORE_KEY", str(remote_dir))  # for the local option, the key is the path
     provider_hparams = ObjectStoreProviderHparams(
         provider='local',
-        key=str(remote_dir),  # for the local option, the key is the path
+        key_environ="OBJECT_STORE_KEY",
         container=".",
     )
     with open(str(remote_dir / "checkpoint.txt"), 'wb') as f: