Repository: meltano/meltano

Issue:
bug: misparsing of State backend URI in S3
### Meltano Version

2.14.0

### Python Version

3.10

### Bug scope

Configuration (settings parsing, validation, etc.)

### Operating System

ECS Fargate, S3 state backend

### Description

Part of the bucket name for the S3 state backend is being cut off, resulting in `AccessDenied` errors when attempting to read and write state.

### Code

_No response_


Discussion:
FYI @seth-meltano I added to the Engineering Board

Resolving patch:
diff --git a/src/meltano/core/state_store/filesystem.py b/src/meltano/core/state_store/filesystem.py
--- a/src/meltano/core/state_store/filesystem.py
+++ b/src/meltano/core/state_store/filesystem.py
@@ -21,6 +21,7 @@
 
 from meltano.core.job_state import JobState
 from meltano.core.state_store.base import StateStoreManager
+from meltano.core.utils import remove_suffix
 
 logger = logging.getLogger(__name__)
 
@@ -87,12 +88,14 @@ def get_reader(self, path: str) -> Iterator[TextIOWrapper]:
         """
         if self.client:
             with open(
-                self.join_path(self.uri.rstrip(self.state_dir), path),
+                self.join_path(remove_suffix(self.uri, (self.state_dir)), path),
                 transport_params={"client": self.client},
             ) as reader:
                 yield reader
         else:
-            with open(self.join_path(self.uri.rstrip(self.state_dir), path)) as reader:
+            with open(
+                self.join_path(remove_suffix(self.uri, self.state_dir), path)
+            ) as reader:
                 yield reader
 
     @contextmanager
@@ -107,14 +110,14 @@ def get_writer(self, path: str) -> Iterator[TextIOWrapper]:
         """
         try:
             with open(
-                self.join_path(self.uri.rstrip(self.state_dir), path),
+                self.join_path(remove_suffix(self.uri, self.state_dir), path),
                 "w+",
                 transport_params={"client": self.client} if self.client else {},
             ) as writer:
                 yield writer
         except NotImplementedError:
             with open(
-                self.join_path(self.uri.rstrip(self.state_dir), path),
+                self.join_path(remove_suffix(self.uri, self.state_dir), path),
                 "w",
                 transport_params={"client": self.client} if self.client else {},
             ) as writer:
diff --git a/src/meltano/core/utils/__init__.py b/src/meltano/core/utils/__init__.py
--- a/src/meltano/core/utils/__init__.py
+++ b/src/meltano/core/utils/__init__.py
@@ -811,3 +811,22 @@ def _deep_merge(a, b, strategies):
             ):
                 break
     return base
+
+
+def remove_suffix(string: str, suffix: str) -> str:
+    """Remove suffix from string.
+
+    Compatible with Python 3.8
+
+    Args:
+        string: the string to remove suffix from
+        suffix: the suffix to remove
+
+    Returns:
+        The changed string
+    """
+    if sys.version_info >= (3, 9):
+        return string.removesuffix(suffix)
+    elif string.endswith(suffix):
+        return string[: -len(suffix)]
+    return string


Test patch:
diff --git a/tests/meltano/core/test_utils.py b/tests/meltano/core/test_utils.py
--- a/tests/meltano/core/test_utils.py
+++ b/tests/meltano/core/test_utils.py
@@ -10,6 +10,7 @@
     flatten,
     nest,
     pop_at_path,
+    remove_suffix,
     set_at_path,
 )
 
@@ -155,3 +156,9 @@ def test_expand_env_vars_nested():
     }
 
     assert expand_env_vars(input_dict, env) == expected_output
+
+
+def test_remove_suffix():
+    assert remove_suffix("a_string", "ing") == "a_str"
+    assert remove_suffix("a_string", "in") == "a_string"
+    assert remove_suffix("a_string", "gni") == "a_string"