Skip to content

Commit 722401d

Browse files
authored
Support interpolation for S3 value retrieval (#6)
1 parent 153680a commit 722401d

File tree

8 files changed

+102
-10
lines changed

8 files changed

+102
-10
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
*.DS_Store
2+
13
# Byte-compiled / optimized / DLL files
24
__pycache__/
35
*.py[cod]

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ See an example [here](https://github.com/adobe/himl#deep-merge-example).
225225
passphrase: "{{ssm.path(/key/coming/from/aws/secrets/store/manager).aws_profile(myprofile)}}"
226226
```
227227

228+
#### [AWS S3](https://aws.amazon.com/s3/)
229+
230+
```yaml
231+
my_value: "{{s3.bucket(my-bucket).path(path/to/file.txt).base64encode(true).aws_profile(myprofile)}}"
232+
```
233+
228234
#### [Vault](https://www.vaultproject.io/)
229235

230236
Not yet implemented.

examples/complex/default.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ cluster:
1111

1212
# Fetching the secret value at runtime, from a secrets store (in this case AWS SSM).
1313
# passphrase: "{{ssm.path(/key/coming/from/aws/secrets/store/manager).aws_profile(myprofile)}}"
14+
15+
# Fetching the value at runtime from S3
16+
# my_secret: "{{s3.bucket(my-bucket).path(path/to/file.txt).base64encode(true).aws_profile(myprofile)}}"

examples/complex/env=dev/env.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
env: dev
1+
env: dev

himl/inject_secrets.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# OF ANY KIND, either express or implied. See the License for the specific language
99
# governing permissions and limitations under the License.
1010

11+
import re
1112
from .secret_resolvers import AggregatedSecretResolver
1213

1314
try:
@@ -43,7 +44,7 @@ def inject_secret(self, line):
4344
updated_line = line[2:-2]
4445

4546
# parse each key/value (eg. path=my_pwd)
46-
parts = updated_line.split('.')
47+
parts = self.split_dot_not_within_parentheses(updated_line)
4748
if len(parts) <= 1:
4849
return line
4950

@@ -62,3 +63,12 @@ def inject_secret(self, line):
6263
return self.resolver.resolve(secret_type, secret_params)
6364
else:
6465
return line
66+
67+
def split_dot_not_within_parentheses(self, line):
68+
"""
69+
s3.bucket(my-bucket).path(path/to/file.txt).aws_profile(myprofile)
70+
will result in:
71+
['s3', 'bucket(my-bucket)', 'path(path/to/file.txt)']
72+
"""
73+
pattern = r'\.\s*(?![^()]*\))'
74+
return re.split(pattern, line)

himl/secret_resolvers.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
# OF ANY KIND, either express or implied. See the License for the specific language
99
# governing permissions and limitations under the License.
1010

11+
import logging
1112
from .simplessm import SimpleSSM
13+
from .simples3 import SimpleS3
1214

1315

1416
class SecretResolver:
@@ -18,6 +20,11 @@ def supports(self, secret_type):
1820
def resolve(self, secret_type, secret_params):
1921
return None
2022

23+
def get_param_or_exception(self, key, params):
24+
if key not in params:
25+
raise Exception("Could not find required key '{}' in the secret params: {}".format(key, params))
26+
return params[key]
27+
2128

2229
class SSMSecretResolver(SecretResolver):
2330
def __init__(self, default_aws_profile=None):
@@ -29,17 +36,33 @@ def supports(self, secret_type):
2936
def resolve(self, secret_type, secret_params):
3037
aws_profile = secret_params.get("aws_profile", self.default_aws_profile)
3138
if not aws_profile:
32-
raise Exception("Could not find the aws_profile in the secret params: {}".format(secret_params))
39+
raise Exception("Could not find the aws_profile in the secret params for SSM secret: {}".format(secret_params))
3340

3441
path = self.get_param_or_exception("path", secret_params)
3542
region_name = secret_params.get("region_name", "us-east-1")
3643
ssm = SimpleSSM(aws_profile, region_name)
3744
return ssm.get(path)
3845

39-
def get_param_or_exception(self, key, params):
40-
if key not in params:
41-
raise Exception("Could not find required key '{}' in the secret params: {}".format(key, params))
42-
return params[key]
46+
47+
class S3SecretResolver(SecretResolver):
48+
def __init__(self, default_aws_profile=None):
49+
self.default_aws_profile = default_aws_profile
50+
51+
def supports(self, secret_type):
52+
return secret_type == "s3"
53+
54+
def resolve(self, secret_type, secret_params):
55+
aws_profile = secret_params.get("aws_profile", self.default_aws_profile)
56+
if not aws_profile:
57+
raise Exception("Could not find the aws_profile in the secret params for S3 secret: {}".format(secret_params))
58+
59+
bucket = self.get_param_or_exception("bucket", secret_params)
60+
path = self.get_param_or_exception("path", secret_params)
61+
region_name = secret_params.get("region_name", "us-east-1")
62+
base64Encode = secret_params.get("base64encode", False)
63+
base64Encode = base64Encode == 'true'
64+
s3 = SimpleS3(aws_profile, region_name)
65+
return s3.get(bucket, path, base64Encode)
4366

4467

4568
# TODO - vault resolver
@@ -54,7 +77,7 @@ def resolve(self, secret_type, secret_params):
5477
class AggregatedSecretResolver(SecretResolver):
5578

5679
def __init__(self, default_aws_profile=None):
57-
self.secret_resolvers = (SSMSecretResolver(default_aws_profile), VaultSecretResolver())
80+
self.secret_resolvers = (SSMSecretResolver(default_aws_profile), S3SecretResolver(default_aws_profile), VaultSecretResolver())
5881

5982
def supports(self, secret_type):
6083
return any([resolver.supports(secret_type) for resolver in self.secret_resolvers])

himl/simples3.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2019 Adobe. All rights reserved.
2+
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License. You may obtain a copy
4+
# of the License at http://www.apache.org/licenses/LICENSE-2.0
5+
6+
# Unless required by applicable law or agreed to in writing, software distributed under
7+
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
# OF ANY KIND, either express or implied. See the License for the specific language
9+
# governing permissions and limitations under the License.
10+
11+
import boto3
12+
import logging
13+
import os
14+
from botocore.exceptions import ClientError
15+
16+
17+
logger = logging.getLogger(__name__)
18+
19+
class SimpleS3(object):
20+
def __init__(self, aws_profile, region_name):
21+
self.aws_profile = aws_profile
22+
self.region_name = region_name
23+
24+
def get(self, bucket_name, bucket_key, base64Encode=False):
25+
try:
26+
logger.info("Resolving S3 object for bucket %s, key '%s' on profile %s in region %s",
27+
bucket_name, bucket_key, self.aws_profile, self.region_name)
28+
client = self.get_s3_client()
29+
bucket_object = client.get_object(Bucket=bucket_name, Key=bucket_key)["Body"].read()
30+
return self.parse_data(bucket_object, base64Encode)
31+
except ClientError as e:
32+
raise Exception(
33+
'Error while trying to read S3 value for bucket_name %s, bucket_key: %s - %s'
34+
% (bucket_name, bucket_key, e.response['Error']['Code']))
35+
36+
def parse_data(self, bucket_object, base64Encode):
37+
if base64Encode:
38+
import base64
39+
encodedBytes = base64.b64encode(bucket_object)
40+
return str(encodedBytes, "utf-8")
41+
return bucket_object
42+
43+
def get_s3_client(self):
44+
session = boto3.session.Session(profile_name=self.aws_profile)
45+
return session.client('s3')

himl/simplessm.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
# OF ANY KIND, either express or implied. See the License for the specific language
99
# governing permissions and limitations under the License.
1010

11-
import os
12-
1311
import boto3
12+
import logging
13+
import os
1414
from botocore.exceptions import ClientError
1515

1616

17+
logger = logging.getLogger(__name__)
18+
1719
class SimpleSSM(object):
1820
def __init__(self, aws_profile, region_name):
1921
self.initial_aws_profile = os.getenv('AWS_PROFILE', None)
@@ -23,6 +25,7 @@ def __init__(self, aws_profile, region_name):
2325
def get(self, key):
2426
client = self.get_ssm_client()
2527
try:
28+
logger.info("Resolving SSM secret for key '%s' on profile %s in region %s", key, self.aws_profile, self.region_name)
2629
return client.get_parameter(Name=key, WithDecryption=True).get("Parameter").get("Value")
2730
except ClientError as e:
2831
raise Exception(

0 commit comments

Comments
 (0)