outerbounds 0.3.173rc0__py3-none-any.whl → 0.3.175__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- outerbounds/command_groups/apps_cli.py +5 -1
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175.dist-info}/METADATA +3 -3
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175.dist-info}/RECORD +5 -19
- outerbounds/apps/__init__.py +0 -0
- outerbounds/apps/app_cli.py +0 -519
- outerbounds/apps/app_config.py +0 -308
- outerbounds/apps/artifacts.py +0 -0
- outerbounds/apps/capsule.py +0 -382
- outerbounds/apps/code_package/__init__.py +0 -3
- outerbounds/apps/code_package/code_packager.py +0 -612
- outerbounds/apps/code_package/examples.py +0 -125
- outerbounds/apps/config_schema.yaml +0 -194
- outerbounds/apps/dependencies.py +0 -115
- outerbounds/apps/deployer.py +0 -0
- outerbounds/apps/secrets.py +0 -164
- outerbounds/apps/utils.py +0 -228
- outerbounds/apps/validations.py +0 -34
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175.dist-info}/entry_points.txt +0 -0
@@ -1,125 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Examples demonstrating how to use the code packager abstraction.
|
3
|
-
|
4
|
-
This file provides usage examples for the code packager classes.
|
5
|
-
These examples are for documentation purposes and are not meant to be run directly.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import os
|
9
|
-
import sys
|
10
|
-
from io import BytesIO
|
11
|
-
from typing import List, Dict, Any, Callable, Optional
|
12
|
-
|
13
|
-
from .code_packager import CodePackager
|
14
|
-
|
15
|
-
|
16
|
-
def basic_usage_example(datastore_type: str = "s3") -> None:
|
17
|
-
"""
|
18
|
-
Basic usage example with ContentAddressedStore.
|
19
|
-
|
20
|
-
This example shows how to:
|
21
|
-
1. Define paths to include in a package
|
22
|
-
2. Create a package using CodePackager's default packaging
|
23
|
-
3. Store the package using ContentAddressedStore directly
|
24
|
-
4. Generate a download command
|
25
|
-
|
26
|
-
Parameters
|
27
|
-
----------
|
28
|
-
datastore_type : str, default "s3"
|
29
|
-
The type of datastore to use: "s3", "azure", "gs", or "local"
|
30
|
-
"""
|
31
|
-
# Define the paths to include in the package
|
32
|
-
paths_to_include = ["./"]
|
33
|
-
|
34
|
-
# Define which file suffixes to include
|
35
|
-
file_suffixes = [".py", ".md"]
|
36
|
-
|
37
|
-
# Create metadata for the package
|
38
|
-
metadata = {"example": True, "timestamp": "2023-01-01T00:00:00Z"}
|
39
|
-
|
40
|
-
# Initialize the packager with datastore configuration
|
41
|
-
packager = CodePackager(
|
42
|
-
datastore_type=datastore_type,
|
43
|
-
code_package_prefix="my-custom-prefix", # Optional
|
44
|
-
)
|
45
|
-
|
46
|
-
# Store the package with packaging parameters
|
47
|
-
package_url, package_key = packager.store(
|
48
|
-
paths_to_include=paths_to_include,
|
49
|
-
file_suffixes=file_suffixes,
|
50
|
-
metadata=metadata,
|
51
|
-
)
|
52
|
-
|
53
|
-
# Generate a download command
|
54
|
-
download_cmd = CodePackager.get_download_cmd(
|
55
|
-
package_url=package_url,
|
56
|
-
datastore_type=datastore_type,
|
57
|
-
target_file="my_package.tar",
|
58
|
-
)
|
59
|
-
|
60
|
-
# Generate complete package commands for downloading and setup
|
61
|
-
package_commands = packager.get_package_commands(
|
62
|
-
code_package_url=package_url,
|
63
|
-
target_file="my_package.tar",
|
64
|
-
working_dir="my_app",
|
65
|
-
)
|
66
|
-
|
67
|
-
# Print some information
|
68
|
-
print(f"Package URL: {package_url}")
|
69
|
-
print(f"Package Key: {package_key}")
|
70
|
-
print(f"Download Command: {download_cmd}")
|
71
|
-
print(f"Complete package commands: {package_commands}")
|
72
|
-
|
73
|
-
|
74
|
-
def usage_with_custom_package_function(datastore_type: str = "s3") -> None:
|
75
|
-
"""
|
76
|
-
Example of using the CodePackager with a custom package creation function.
|
77
|
-
|
78
|
-
Parameters
|
79
|
-
----------
|
80
|
-
datastore_type : str, default "s3"
|
81
|
-
The type of datastore to use: "s3", "azure", "gs", or "local"
|
82
|
-
"""
|
83
|
-
|
84
|
-
# Define a custom package function
|
85
|
-
def create_custom_package():
|
86
|
-
# This is a simple example - in real use, you might create a more complex package
|
87
|
-
from io import BytesIO
|
88
|
-
import tarfile
|
89
|
-
import time
|
90
|
-
|
91
|
-
buf = BytesIO()
|
92
|
-
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
93
|
-
# Add a simple file to the tarball
|
94
|
-
content = b"print('Hello, custom package!')"
|
95
|
-
info = tarfile.TarInfo(name="hello.py")
|
96
|
-
info.size = len(content)
|
97
|
-
info.mtime = int(time.time())
|
98
|
-
file_object = BytesIO(content)
|
99
|
-
tar.addfile(info, file_object)
|
100
|
-
|
101
|
-
return bytearray(buf.getvalue())
|
102
|
-
|
103
|
-
# Initialize the packager with datastore configuration
|
104
|
-
packager = CodePackager(datastore_type=datastore_type)
|
105
|
-
|
106
|
-
# Store the package with the custom package function
|
107
|
-
package_url, package_key = packager.store(package_create_fn=create_custom_package)
|
108
|
-
|
109
|
-
# Generate a download command
|
110
|
-
download_cmd = CodePackager.get_download_cmd(
|
111
|
-
package_url=package_url,
|
112
|
-
datastore_type=datastore_type,
|
113
|
-
target_file="custom_package.tar",
|
114
|
-
)
|
115
|
-
|
116
|
-
# Generate complete package commands
|
117
|
-
package_commands = packager.get_package_commands(
|
118
|
-
code_package_url=package_url,
|
119
|
-
)
|
120
|
-
|
121
|
-
# Print some information
|
122
|
-
print(f"Package URL: {package_url}")
|
123
|
-
print(f"Package Key: {package_key}")
|
124
|
-
print(f"Download Command: {download_cmd}")
|
125
|
-
print(f"Complete commands: {package_commands}")
|
@@ -1,194 +0,0 @@
|
|
1
|
-
|
2
|
-
title: Outerbounds App Configuration Schema
|
3
|
-
description: |
|
4
|
-
Schema for defining Outerbounds Apps configuration. This schema is what we will end up using on the CLI/programmatic interface.
|
5
|
-
All the properties in this schema will then translate into an IR that will have resolved information from the
|
6
|
-
version: 1.0.0
|
7
|
-
type: object
|
8
|
-
required:
|
9
|
-
- name
|
10
|
-
- port
|
11
|
-
properties:
|
12
|
-
name: # Only used in `deploy` command
|
13
|
-
allow_union: true # Allow overriding name from the CLI.
|
14
|
-
type: string
|
15
|
-
description: The name of the app to deploy.
|
16
|
-
maxLength: 20 # todo: check if we should allow a larger length.
|
17
|
-
example: "myapp"
|
18
|
-
port: # Only used in `deploy` command
|
19
|
-
allow_union: false
|
20
|
-
type: integer
|
21
|
-
description: Port where the app is hosted. When deployed this will be port on which we will deploy the app.
|
22
|
-
minimum: 1
|
23
|
-
maximum: 65535
|
24
|
-
example: 8000
|
25
|
-
tags: # Only used in `deploy` command
|
26
|
-
allow_union: true
|
27
|
-
type: array
|
28
|
-
description: The tags of the app to deploy.
|
29
|
-
items:
|
30
|
-
type: string
|
31
|
-
example: ["production", "v1.0"]
|
32
|
-
image: # Only used in `deploy` command
|
33
|
-
allow_union: true # We will overrwite the image if specified on the CLI.
|
34
|
-
type: string
|
35
|
-
description: The Docker image to deploy with the App.
|
36
|
-
example: "python:3.10-slim"
|
37
|
-
secrets: # Used in `run` command
|
38
|
-
allow_union: true
|
39
|
-
type: array
|
40
|
-
description: Outerbounds integrations to attach to the app. You can use the value you set in the `@secrets` decorator in your code.
|
41
|
-
items:
|
42
|
-
type: string
|
43
|
-
example: ["outerbounds.hf-token"]
|
44
|
-
environment: # Used in `run` command
|
45
|
-
# Todo: So this part might not be best on the CLI. We should probably have a better way to handle this.
|
46
|
-
# In simplicity, we can just JSON dump anything that looks like a dict/list/
|
47
|
-
allow_union: true
|
48
|
-
type: object
|
49
|
-
description: Environment variables to deploy with the App.
|
50
|
-
additionalProperties:
|
51
|
-
oneOf:
|
52
|
-
- type: string
|
53
|
-
- type: number
|
54
|
-
- type: boolean
|
55
|
-
- type: object
|
56
|
-
- type: array # When users give arrays, or objects, we need to JSON dump them. Users need to be aware of this.
|
57
|
-
example:
|
58
|
-
DEBUG: true
|
59
|
-
DATABASE_CONFIG: {"host": "localhost", "port": 5432}
|
60
|
-
ALLOWED_ORIGINS: ["http://localhost:3000", "https://myapp.com"]
|
61
|
-
dependencies: # Used in `run` command
|
62
|
-
allow_union: false
|
63
|
-
type: object
|
64
|
-
description: |
|
65
|
-
The dependencies to attach to the app. Only one of the properties can be specified.
|
66
|
-
properties:
|
67
|
-
from_requirements_file:
|
68
|
-
type: string
|
69
|
-
description: The path to the requirements.txt file to attach to the app.
|
70
|
-
example: "requirements.txt"
|
71
|
-
from_pyproject_toml:
|
72
|
-
type: string
|
73
|
-
description: The path to the pyproject.toml file to attach to the app.
|
74
|
-
example: "pyproject.toml"
|
75
|
-
python:
|
76
|
-
type: string
|
77
|
-
description: |
|
78
|
-
The Python version to use for the app.
|
79
|
-
example: "3.10"
|
80
|
-
pypi:
|
81
|
-
type: object
|
82
|
-
description: |
|
83
|
-
A dictionary of pypi dependencies to attach to the app.
|
84
|
-
The key is the package name and the value is the version.
|
85
|
-
example:
|
86
|
-
numpy: 1.23.0
|
87
|
-
pandas: ""
|
88
|
-
conda:
|
89
|
-
type: object
|
90
|
-
description: |
|
91
|
-
A dictionary of pypi dependencies to attach to the app.
|
92
|
-
The key is the package name and the value is the version.
|
93
|
-
example:
|
94
|
-
numpy: 1.23.0
|
95
|
-
pandas: ""
|
96
|
-
package:
|
97
|
-
allow_union: false
|
98
|
-
type: object
|
99
|
-
description: |
|
100
|
-
Configurations associated with packaging the app.
|
101
|
-
properties:
|
102
|
-
src_path:
|
103
|
-
type: string
|
104
|
-
description: The path to the source code to deploy with the App.
|
105
|
-
example: "./"
|
106
|
-
suffixes:
|
107
|
-
type: array
|
108
|
-
description: |
|
109
|
-
A list of suffixes to add to the source code to deploy with the App.
|
110
|
-
items:
|
111
|
-
type: string
|
112
|
-
example: [".py", ".ipynb"]
|
113
|
-
|
114
|
-
commands: # Used in `run` command
|
115
|
-
allow_union: false
|
116
|
-
type: array
|
117
|
-
description: A list of commands to run the app with. Cannot be configured from the CLI. Only used in `run` command.
|
118
|
-
items:
|
119
|
-
type: string
|
120
|
-
example: ["python app.py", "python app.py --foo bar"]
|
121
|
-
resources: # Only used in `deploy` command
|
122
|
-
allow_union: true # You can override CPU/Memory/GPU/Storage from the CLI.
|
123
|
-
type: object
|
124
|
-
properties:
|
125
|
-
cpu:
|
126
|
-
type: string
|
127
|
-
description: CPU resource request and limit.
|
128
|
-
example: "500m"
|
129
|
-
default: "1"
|
130
|
-
memory:
|
131
|
-
type: string
|
132
|
-
description: Memory resource request and limit.
|
133
|
-
example: "512Mi"
|
134
|
-
default: "4Gi"
|
135
|
-
gpu:
|
136
|
-
type: string
|
137
|
-
description: GPU resource request and limit.
|
138
|
-
example: "1"
|
139
|
-
storage:
|
140
|
-
type: string
|
141
|
-
description: Storage resource request and limit.
|
142
|
-
example: "1Gi"
|
143
|
-
default: "10Gi"
|
144
|
-
health_check: # Can be used in `run` command
|
145
|
-
type: object
|
146
|
-
# `allow_union` property means that any object in this field will be done a union with the config file if something is provided on commanline.
|
147
|
-
# If it is set to false, then we should throw an exception if the CLI is trying to override something specified in the config file.
|
148
|
-
# We will only allow unions in certains options. The rest will not allow any unions and only need to be specified in one place.
|
149
|
-
allow_union: false
|
150
|
-
properties:
|
151
|
-
enabled:
|
152
|
-
type: boolean
|
153
|
-
description: Whether to enable health checks.
|
154
|
-
example: true
|
155
|
-
default: false
|
156
|
-
path:
|
157
|
-
type: string
|
158
|
-
description: The path for health checks.
|
159
|
-
example: "/health"
|
160
|
-
initial_delay_seconds:
|
161
|
-
type: integer
|
162
|
-
description: Number of seconds to wait before performing the first health check.
|
163
|
-
example: 10
|
164
|
-
period_seconds:
|
165
|
-
type: integer
|
166
|
-
description: How often to perform the health check.
|
167
|
-
example: 30
|
168
|
-
compute_pools: # Only used in `deploy` command
|
169
|
-
allow_union: true
|
170
|
-
type: array
|
171
|
-
description: |
|
172
|
-
A list of compute pools to deploy the app to.
|
173
|
-
items:
|
174
|
-
type: string
|
175
|
-
example: ["default", "large"]
|
176
|
-
auth: # Only used in `deploy` command
|
177
|
-
allow_union: false
|
178
|
-
type: object
|
179
|
-
description: |
|
180
|
-
Auth related configurations.
|
181
|
-
properties:
|
182
|
-
type:
|
183
|
-
type: string
|
184
|
-
description: |
|
185
|
-
The type of authentication to use for the app.
|
186
|
-
enum: [API, SSO]
|
187
|
-
public:
|
188
|
-
type: boolean
|
189
|
-
description: |
|
190
|
-
Whether the app is public or not.
|
191
|
-
default: true
|
192
|
-
# There is an allowed perimeters property
|
193
|
-
# But that needs a little more thought on how
|
194
|
-
# to expose.
|
outerbounds/apps/dependencies.py
DELETED
@@ -1,115 +0,0 @@
|
|
1
|
-
import copy
|
2
|
-
import json
|
3
|
-
import os
|
4
|
-
import shutil
|
5
|
-
import sys
|
6
|
-
import tempfile
|
7
|
-
from hashlib import sha256
|
8
|
-
from typing import List, Optional, Callable, Any
|
9
|
-
from .app_config import AppConfig
|
10
|
-
from .utils import TODOException
|
11
|
-
from metaflow.metaflow_config import (
|
12
|
-
get_pinned_conda_libs,
|
13
|
-
DEFAULT_DATASTORE,
|
14
|
-
KUBERNETES_CONTAINER_IMAGE,
|
15
|
-
)
|
16
|
-
from collections import namedtuple
|
17
|
-
|
18
|
-
BakingStatus = namedtuple(
|
19
|
-
"BakingStatus", ["image_should_be_baked", "python_path", "resolved_image"]
|
20
|
-
)
|
21
|
-
|
22
|
-
|
23
|
-
class ImageBakingException(Exception):
|
24
|
-
pass
|
25
|
-
|
26
|
-
|
27
|
-
def _safe_open_file(path: str):
|
28
|
-
if not os.path.exists(path):
|
29
|
-
raise TODOException(f"File does not exist: {path}")
|
30
|
-
try:
|
31
|
-
with open(path, "r") as f:
|
32
|
-
return f.read()
|
33
|
-
except Exception as e:
|
34
|
-
raise TODOException(f"Failed to open file: {e}")
|
35
|
-
|
36
|
-
|
37
|
-
def bake_deployment_image(
|
38
|
-
app_config: AppConfig,
|
39
|
-
cache_file_path: str,
|
40
|
-
logger: Optional[Callable[[str], Any]] = None,
|
41
|
-
) -> BakingStatus:
|
42
|
-
# When do we bake an image?
|
43
|
-
# 1. When the user has specified something like `pypi`/`conda`
|
44
|
-
# 2, When the user has specified something like `from_requirements`/ `from_pyproject`
|
45
|
-
# TODO: add parsers for the pyproject/requirements stuff.
|
46
|
-
from metaflow.ob_internal import bake_image
|
47
|
-
from metaflow.plugins.pypi.parsers import (
|
48
|
-
requirements_txt_parser,
|
49
|
-
pyproject_toml_parser,
|
50
|
-
)
|
51
|
-
|
52
|
-
image = app_config.get("image", KUBERNETES_CONTAINER_IMAGE)
|
53
|
-
python_version = "%d.%d.%d" % sys.version_info[:3]
|
54
|
-
|
55
|
-
dependencies = app_config.get_state("dependencies", {})
|
56
|
-
pypi_packages = {}
|
57
|
-
conda_packages = {}
|
58
|
-
|
59
|
-
parsed_packages = {}
|
60
|
-
|
61
|
-
if dependencies.get("from_requirements_file"):
|
62
|
-
parsed_packages = requirements_txt_parser(
|
63
|
-
_safe_open_file(dependencies.get("from_requirements_file"))
|
64
|
-
)
|
65
|
-
pypi_packages = parsed_packages.get("packages", {})
|
66
|
-
python_version = parsed_packages.get("python_version", python_version)
|
67
|
-
|
68
|
-
elif dependencies.get("from_pyproject_toml"):
|
69
|
-
parsed_packages = pyproject_toml_parser(
|
70
|
-
_safe_open_file(dependencies.get("from_pyproject_toml"))
|
71
|
-
)
|
72
|
-
pypi_packages = parsed_packages.get("packages", {})
|
73
|
-
python_version = parsed_packages.get("python_version", python_version)
|
74
|
-
|
75
|
-
elif "pypi" in dependencies:
|
76
|
-
pypi_packages = dependencies.get("pypi", {})
|
77
|
-
|
78
|
-
if "conda" in dependencies:
|
79
|
-
conda_packages = dependencies.get("conda", {})
|
80
|
-
if "python" in dependencies:
|
81
|
-
python_version = dependencies.get("python", python_version)
|
82
|
-
|
83
|
-
python_packages_exist = len(pypi_packages) > 0 or len(conda_packages) > 0
|
84
|
-
if (not python_packages_exist) or app_config.get_state("skip_dependencies", False):
|
85
|
-
# Inform the user that no dependencies are being used.
|
86
|
-
if app_config.get_state("skip_dependencies", False):
|
87
|
-
logger(
|
88
|
-
"⏭️ Skipping baking dependencies into the image based on the --no-deps flag."
|
89
|
-
)
|
90
|
-
# TODO: Handle this a little more nicely.
|
91
|
-
return BakingStatus(
|
92
|
-
image_should_be_baked=False, resolved_image=image, python_path="python"
|
93
|
-
)
|
94
|
-
|
95
|
-
pinned_conda_libs = get_pinned_conda_libs(python_version, DEFAULT_DATASTORE)
|
96
|
-
pypi_packages.update(pinned_conda_libs)
|
97
|
-
_reference = app_config.get("name", "default")
|
98
|
-
# `image` cannot be None. If by change it is none, FB will fart.
|
99
|
-
fb_response = bake_image(
|
100
|
-
cache_file_path=cache_file_path,
|
101
|
-
pypi_packages=pypi_packages,
|
102
|
-
conda_packages=conda_packages,
|
103
|
-
ref=_reference,
|
104
|
-
python=python_version,
|
105
|
-
base_image=image,
|
106
|
-
logger=logger,
|
107
|
-
)
|
108
|
-
if fb_response.failure:
|
109
|
-
raise ImageBakingException(f"Failed to bake image: {fb_response.response}")
|
110
|
-
|
111
|
-
return BakingStatus(
|
112
|
-
image_should_be_baked=True,
|
113
|
-
resolved_image=fb_response.container_image,
|
114
|
-
python_path=fb_response.python_path,
|
115
|
-
)
|
outerbounds/apps/deployer.py
DELETED
File without changes
|
outerbounds/apps/secrets.py
DELETED
@@ -1,164 +0,0 @@
|
|
1
|
-
from typing import Dict
|
2
|
-
|
3
|
-
import base64
|
4
|
-
import json
|
5
|
-
import requests
|
6
|
-
import random
|
7
|
-
import time
|
8
|
-
import sys
|
9
|
-
|
10
|
-
from .utils import safe_requests_wrapper, TODOException
|
11
|
-
|
12
|
-
|
13
|
-
class OuterboundsSecretsException(Exception):
|
14
|
-
pass
|
15
|
-
|
16
|
-
|
17
|
-
class SecretNotFound(OuterboundsSecretsException):
|
18
|
-
pass
|
19
|
-
|
20
|
-
|
21
|
-
class OuterboundsSecretsApiResponse:
|
22
|
-
def __init__(self, response):
|
23
|
-
self.response = response
|
24
|
-
|
25
|
-
@property
|
26
|
-
def secret_resource_id(self):
|
27
|
-
return self.response["secret_resource_id"]
|
28
|
-
|
29
|
-
@property
|
30
|
-
def secret_backend_type(self):
|
31
|
-
return self.response["secret_backend_type"]
|
32
|
-
|
33
|
-
|
34
|
-
class SecretRetriever:
|
35
|
-
def get_secret_as_dict(self, secret_id, options={}, role=None):
|
36
|
-
"""
|
37
|
-
Supports a special way of specifying secrets sources in outerbounds using the format:
|
38
|
-
@secrets(sources=["outerbounds.<integrations_name>"])
|
39
|
-
|
40
|
-
When invoked it makes a requests to the integrations secrets metadata endpoint on the
|
41
|
-
keywest server to get the cloud resource id for a secret. It then uses that to invoke
|
42
|
-
secrets manager on the core oss and returns the secrets.
|
43
|
-
"""
|
44
|
-
headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
|
45
|
-
perimeter, integrations_url = self._get_secret_configs()
|
46
|
-
integration_name = secret_id
|
47
|
-
request_payload = {
|
48
|
-
"perimeter_name": perimeter,
|
49
|
-
"integration_name": integration_name,
|
50
|
-
}
|
51
|
-
response = self._make_request(integrations_url, headers, request_payload)
|
52
|
-
secret_resource_id = response.secret_resource_id
|
53
|
-
secret_backend_type = response.secret_backend_type
|
54
|
-
|
55
|
-
from metaflow.plugins.secrets.secrets_decorator import (
|
56
|
-
get_secrets_backend_provider,
|
57
|
-
)
|
58
|
-
|
59
|
-
secrets_provider = get_secrets_backend_provider(secret_backend_type)
|
60
|
-
secret_dict = secrets_provider.get_secret_as_dict(
|
61
|
-
secret_resource_id, options={}, role=role
|
62
|
-
)
|
63
|
-
|
64
|
-
# Outerbounds stores secrets as binaries. Hence we expect the returned secret to be
|
65
|
-
# {<cloud-secret-name>: <base64 encoded full secret>}. We decode the secret here like:
|
66
|
-
# 1. decode the base64 encoded full secret
|
67
|
-
# 2. load the decoded secret as a json
|
68
|
-
# 3. decode the base64 encoded values in the dict
|
69
|
-
# 4. return the decoded dict
|
70
|
-
binary_secret = next(iter(secret_dict.values()))
|
71
|
-
return self._decode_secret(binary_secret)
|
72
|
-
|
73
|
-
def _is_base64_encoded(self, data):
|
74
|
-
try:
|
75
|
-
if isinstance(data, str):
|
76
|
-
# Check if the string can be base64 decoded
|
77
|
-
base64.b64decode(data).decode("utf-8")
|
78
|
-
return True
|
79
|
-
return False
|
80
|
-
except Exception:
|
81
|
-
return False
|
82
|
-
|
83
|
-
def _decode_secret(self, secret):
|
84
|
-
try:
|
85
|
-
result = {}
|
86
|
-
secret_str = secret
|
87
|
-
if self._is_base64_encoded(secret):
|
88
|
-
# we check if the secret string is base64 encoded because the returned secret from
|
89
|
-
# AWS secret manager is base64 encoded while the secret from GCP is not
|
90
|
-
secret_str = base64.b64decode(secret).decode("utf-8")
|
91
|
-
|
92
|
-
secret_dict = json.loads(secret_str)
|
93
|
-
for key, value in secret_dict.items():
|
94
|
-
result[key] = base64.b64decode(value).decode("utf-8")
|
95
|
-
|
96
|
-
return result
|
97
|
-
except Exception as e:
|
98
|
-
raise OuterboundsSecretsException(f"Error decoding secret: {e}")
|
99
|
-
|
100
|
-
def _get_secret_configs(self):
|
101
|
-
from metaflow_extensions.outerbounds.remote_config import init_config
|
102
|
-
from os import environ
|
103
|
-
|
104
|
-
conf = init_config()
|
105
|
-
if "OBP_PERIMETER" in conf:
|
106
|
-
perimeter = conf["OBP_PERIMETER"]
|
107
|
-
else:
|
108
|
-
# if the perimeter is not in metaflow config, try to get it from the environment
|
109
|
-
perimeter = environ.get("OBP_PERIMETER", "")
|
110
|
-
|
111
|
-
if "OBP_INTEGRATIONS_URL" in conf:
|
112
|
-
integrations_url = conf["OBP_INTEGRATIONS_URL"]
|
113
|
-
else:
|
114
|
-
# if the integrations is not in metaflow config, try to get it from the environment
|
115
|
-
integrations_url = environ.get("OBP_INTEGRATIONS_URL", "")
|
116
|
-
|
117
|
-
if not perimeter:
|
118
|
-
raise OuterboundsSecretsException(
|
119
|
-
"No perimeter set. Please make sure to run `outerbounds configure <...>` command which can be found on the Ourebounds UI or reach out to your Outerbounds support team."
|
120
|
-
)
|
121
|
-
|
122
|
-
if not integrations_url:
|
123
|
-
raise OuterboundsSecretsException(
|
124
|
-
"No integrations url set. Please notify your Outerbounds support team about this issue."
|
125
|
-
)
|
126
|
-
|
127
|
-
integrations_secrets_metadata_url = f"{integrations_url}/secrets/metadata"
|
128
|
-
return perimeter, integrations_secrets_metadata_url
|
129
|
-
|
130
|
-
def _make_request(self, url, headers: Dict, payload: Dict):
|
131
|
-
try:
|
132
|
-
from metaflow.metaflow_config import SERVICE_HEADERS
|
133
|
-
|
134
|
-
request_headers = {**headers, **(SERVICE_HEADERS or {})}
|
135
|
-
except ImportError:
|
136
|
-
raise TODOException(
|
137
|
-
"Failed to create capsule: No Metaflow service headers found"
|
138
|
-
)
|
139
|
-
|
140
|
-
response = safe_requests_wrapper(
|
141
|
-
requests.get,
|
142
|
-
url,
|
143
|
-
data=json.dumps(payload),
|
144
|
-
headers=request_headers,
|
145
|
-
conn_error_retries=5,
|
146
|
-
retryable_status_codes=[409],
|
147
|
-
)
|
148
|
-
self._handle_error_response(response)
|
149
|
-
return OuterboundsSecretsApiResponse(response.json())
|
150
|
-
|
151
|
-
@staticmethod
|
152
|
-
def _handle_error_response(response: requests.Response):
|
153
|
-
if response.status_code >= 500:
|
154
|
-
raise OuterboundsSecretsException(
|
155
|
-
f"Server error: {response.text}. Please reach out to your Outerbounds support team."
|
156
|
-
)
|
157
|
-
status_code = response.status_code
|
158
|
-
if status_code == 404:
|
159
|
-
raise SecretNotFound(f"Secret not found: {response.text}")
|
160
|
-
|
161
|
-
if status_code >= 400:
|
162
|
-
raise OuterboundsSecretsException(
|
163
|
-
f"status_code={status_code}\t\n\t\t{response.text}"
|
164
|
-
)
|