Repository: iterative/dvc

Issue:
External out with `remote://<missing-remote>/...` url is not handled properly 
```
KeyError: 'augmentation_stages'
  File "repos/parsing/dvcmeat.py", line 51, in parse_dvc
    _parse_metrics(repo, commit)
  File "repos/parsing/dvcmeat.py", line 79, in _parse_metrics
    metrics = repo.metrics.show()[""]
  File "dvc/repo/metrics/__init__.py", line 8, in show
    return show(self.repo, *args, **kwargs)
  File "dvc/repo/__init__.py", line 49, in wrapper
    return f(repo, *args, **kwargs)
  File "dvc/repo/metrics/show.py", line 115, in show
    metrics = _collect_metrics(repo, targets, rev, recursive)
  File "dvc/repo/metrics/show.py", line 38, in _collect_metrics
    metrics, path_infos = collect(
  File "dvc/repo/collect.py", line 87, in collect
    outs: Outputs = _collect_outs(repo, output_filter=output_filter, deps=deps)
  File "dvc/repo/collect.py", line 25, in _collect_outs
    for stage in repo.graph  # using `graph` to ensure graph checks run
  File "funcy/objects.py", line 28, in __get__
    res = instance.__dict__[self.fget.__name__] = self.fget(instance)
  File "dvc/repo/__init__.py", line 417, in graph
    return build_graph(self.stages, self.outs_trie)
  File "funcy/objects.py", line 28, in __get__
    res = instance.__dict__[self.fget.__name__] = self.fget(instance)
  File "dvc/repo/__init__.py", line 439, in stages
    return self.stage.collect_repo(onerror=error_handler)
  File "dvc/repo/stage.py", line 474, in collect_repo
    new_stages = self.load_file(file_path)
  File "dvc/repo/stage.py", line 300, in load_file
    return self.load_all(path)
  File "dvc/repo/stage.py", line 280, in load_all
    return [stages[key] for key in keys]
  File "dvc/repo/stage.py", line 280, in <listcomp>
    return [stages[key] for key in keys]
  File "dvc/stage/loader.py", line 134, in __getitem__
    stage = self.load_stage(
  File "dvc/stage/loader.py", line 99, in load_stage
    stage.outs = lcat(
  File "funcy/seqs.py", line 180, in lcat
    return list(cat(seqs))
  File "dvc/stage/loader.py", line 100, in <genexpr>
    output.load_from_pipeline(stage, data, typ=key)
  File "funcy/decorators.py", line 45, in wrapper
    return deco(call, *dargs, **dkwargs)
  File "funcy/flow.py", line 224, in post_processing
    return func(call())
  File "dvc/output/__init__.py", line 232, in load_from_pipeline
    yield _get(
  File "dvc/output/__init__.py", line 80, in _get
    fs = get_cloud_fs(stage.repo, name=parsed.netloc)
  File "dvc/fs/__init__.py", line 85, in get_cloud_fs
    remote_conf = get_fs_config(repo.config, **kwargs)
  File "dvc/fs/__init__.py", line 44, in get_fs_config
    remote_conf = config["remote"][name.lower()]
```


[Sentry report](https://sentry.io/organizations/iterative/issues/2422423621/?project=5220519&referrer=slack)

In short we get `KeyError` from `get_cloud_fs() -> get_fs_config()`, which is not an adequate error handling.


Discussion:
After newer changes in DVC, the way we handle it in Studio becomes more thin, i.e. this `KeyError` we are handling is coming from significantly more nested code now. Can you guys wrap it into a custom exception? @efiop 

Resolving patch:
diff --git a/dvc/config.py b/dvc/config.py
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -20,7 +20,15 @@ def __init__(self, msg):
         super().__init__(f"config file error: {msg}")
 
 
-class NoRemoteError(ConfigError):
+class RemoteConfigError(ConfigError):
+    pass
+
+
+class NoRemoteError(RemoteConfigError):
+    pass
+
+
+class RemoteNotFoundError(RemoteConfigError):
     pass
 
 
diff --git a/dvc/fs/__init__.py b/dvc/fs/__init__.py
--- a/dvc/fs/__init__.py
+++ b/dvc/fs/__init__.py
@@ -40,7 +40,12 @@ def get_fs_cls(remote_conf):
 def get_fs_config(config, **kwargs):
     name = kwargs.get("name")
     if name:
-        remote_conf = config["remote"][name.lower()]
+        try:
+            remote_conf = config["remote"][name.lower()]
+        except KeyError:
+            from dvc.config import RemoteNotFoundError
+
+            raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
     else:
         remote_conf = kwargs
     return _resolve_remote_refs(config, remote_conf)


Test patch:
diff --git a/tests/unit/fs/test_fs.py b/tests/unit/fs/test_fs.py
--- a/tests/unit/fs/test_fs.py
+++ b/tests/unit/fs/test_fs.py
@@ -1,6 +1,7 @@
 import pytest
 
-from dvc.fs import get_fs_cls
+from dvc.config import RemoteNotFoundError
+from dvc.fs import get_fs_cls, get_fs_config
 from dvc.fs.hdfs import HDFSFileSystem
 from dvc.fs.http import HTTPFileSystem
 from dvc.fs.https import HTTPSFileSystem
@@ -29,3 +30,8 @@
 )
 def test_get_fs_cls(url, cls):
     assert get_fs_cls({"url": url}) == cls
+
+
+def test_get_fs_config():
+    with pytest.raises(RemoteNotFoundError):
+        get_fs_config({"remote": {}}, name="myremote")