Skip to content

Commit 53d643d

Browse files
committed
feat: add git sha and remotes info to product metadata by default
- this is for build traceability and provenance Co-authored-by: Nick Rohn <[email protected]>
1 parent 561ecb6 commit 53d643d

8 files changed

+106
-9
lines changed

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ click>=6.2
33
Jinja2>=3.1
44
PyYAML>=3.1
55
docker-py>=1.6.0
6-
requests<2.2.29
6+
requests<=2.5.3
77
requests-toolbelt
88
mock>=2.0.0
99
pexpect>=4.2.1
10+
GitPython>=3.1.32

tile_generator/config.py

+34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env python
2+
import subprocess
23

4+
import git
5+
from git import Repo
36
# tile-generator
47
#
58
# Copyright (c) 2015-Present Pivotal Software, Inc. All Rights Reserved.
@@ -61,6 +64,12 @@
6164
# Normalize Jobs - Ensure that job type, template, and properties are set for
6265
# every job
6366

67+
def _find_git_root():
68+
try:
69+
git_root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"]).decode("utf-8").strip()
70+
return git_root
71+
except subprocess.CalledProcessError as e:
72+
return e
6473

6574
# Inspired by https://gist.github.com/angstwad/bf22d1822c38a92ec0a9
6675
def merge_dict(dct, merge_dct):
@@ -172,6 +181,13 @@ def _validate_base_config(self):
172181
'description': {'type': 'string', 'required': True},
173182
'icon_file': {'type': 'string', 'required': True, 'coerce': _base64_img},
174183
'metadata_version': {'type': 'number', 'default': 1.8},
184+
'git_remotes': {'type': 'list', 'required': False, 'schema': {
185+
'type': 'dict', 'schema': {
186+
'name': {'type': 'string', 'required': True},
187+
'urls': {'type': 'list', 'required': True},
188+
}
189+
}},
190+
'git_sha': {'type': 'string', 'required': False},
175191
'stemcell_criteria': {'type': 'dict', 'default': self.default_stemcell(), 'schema': {
176192
'os': {'type': 'string'}, 'version': {'type': 'string'}}},
177193
'release_overides': {'type': 'dict'},
@@ -455,6 +471,24 @@ def upgrade(self):
455471
print('ERROR - configurable_persistence has been deprecated, use auto_services instead', file=sys.stderr)
456472
sys.exit(1)
457473

474+
475+
def set_git_remotes(self, git_remotes=True):
476+
if not git_remotes:
477+
return
478+
git_root = _find_git_root()
479+
repo = git.Repo(git_root)
480+
481+
remotes = [{"name": remote.name, "urls": [str(url) for url in remote.urls]} for remote in repo.remotes]
482+
print("setting remotes {}".format(remotes))
483+
self['git_remotes'] = remotes
484+
485+
def set_git_sha(self, git_sha=True):
486+
if not git_sha:
487+
return
488+
git_root = _find_git_root()
489+
repo = git.Repo(git_root)
490+
self['git_sha'] = repo.head.commit.hexsha
491+
458492
def set_version(self, version):
459493
if version is None:
460494
version = 'patch'

tile_generator/config_unittest.py

+29-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
16-
16+
import git
1717
import mock
1818
import json
1919
import os
@@ -29,6 +29,16 @@
2929
from .config import Config, merge_dict
3030
from .tile_metadata import TileMetadata
3131
from . import template
32+
from types import SimpleNamespace
33+
34+
35+
class MockRepo:
36+
argument = []
37+
38+
def __init__(self, x):
39+
self.argument.append(x)
40+
self.remotes = [SimpleNamespace(urls=["[email protected]:org/repo.git"], name="origin")]
41+
self.head = SimpleNamespace(commit=SimpleNamespace(hexsha="some-sha"))
3242

3343
@contextmanager
3444
def capture_output():
@@ -61,28 +71,37 @@ def tearDown(self):
6171
self.pre_start_file.close()
6272

6373

64-
@mock.patch('tile_generator.config.Config.read_history')
74+
def side_effects(*args, **kwargs):
75+
side_effects.captured_args = args
76+
side_effects.captured_kwargs = kwargs
77+
78+
@mock.patch('git.Repo', MockRepo)
79+
@mock.patch('tile_generator.config._find_git_root')
6580
@mock.patch('tile_generator.config._base64_img')
81+
@mock.patch('tile_generator.config.Config.read_history')
6682
class TestUltimateForm(BaseTest):
67-
def test_diff_final_config_obj(self, mock_read_history, mock_base64_img):
83+
def test_diff_final_config_obj(self, mock_read_history, mock_base64_img, mock__find_git_root):
6884
test_path = os.path.dirname(__file__)
6985
cfg_file_path = os.path.join(test_path, '../sample/tile.yml')
86+
mock__find_git_root.return_value = "/fake/git/root"
7087

7188
with mock.patch('tile_generator.config.CONFIG_FILE', cfg_file_path):
7289
cfg = self.config.read()
7390
cfg.set_version(None)
91+
cfg.set_git_remotes(True)
7492
cfg.set_verbose(False)
7593
cfg.set_sha1(False)
7694
cfg.set_cache(None)
77-
95+
cfg.set_git_sha(True)
96+
7897
with open(test_path + '/test_config_generated_output.json', 'w') as f:
7998
generated_json = json.dumps(cfg, sort_keys=True, indent=2)
8099
f.write(generated_json)
81100
generated_output = json.loads(generated_json)
82101

83102
with open(test_path + '/test_config_expected_output.json', 'r') as f:
84103
expected_output = json.load(f)
85-
104+
86105
# Change the paths to files to be consistent
87106
self.fix_path(generated_output)
88107
self.fix_path(expected_output)
@@ -97,10 +116,14 @@ def test_diff_final_config_obj(self, mock_read_history, mock_base64_img):
97116
expected = json.dumps(expected_output, indent=2).split('\n')
98117
generated = json.dumps(generated_output, indent=2).split('\n')
99118

119+
self.assertGreater(len(MockRepo.argument), 0)
120+
self.assertEqual(MockRepo.argument[-1], '/fake/git/root')
121+
100122
self.assertEqual(len(expected), len(generated))
101123
for line in expected:
102124
self.assertIn(line, generated)
103125

126+
104127
def fix_path(self, obj):
105128
for release in obj['releases'].values():
106129
for package in release.get('packages', []):
@@ -169,7 +192,7 @@ def deep_comparer(self, expected, given, path):
169192
'Expected to have the value:\n%s\nHowever, instead got:\n%s' % (expected, given)))
170193

171194
# mock_read_history, mock_base64_img are not used in this method
172-
def test_metadata_diff(self, mock_read_history, mock_base64_img):
195+
def test_metadata_diff(self, mock_read_history, mock_base64_img, mock__find_git_root):
173196
test_path = os.path.dirname(__file__)
174197
cfg_file_path = os.path.join(test_path, '../sample/tile.yml')
175198

tile_generator/templates/tile/metadata.yml

+8
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,11 @@ update:
5252
provides_product_versions:
5353
- name: {{ tile_metadata.base.name }}
5454
version: '{{ tile_metadata.base.product_version }}'
55+
56+
{% if tile_metadata.git_sha %}
57+
git_sha: {{ tile_metadata.git_sha }}
58+
{% endif %}
59+
{% if tile_metadata.git_remotes %}
60+
git_remotes:
61+
{{ tile_metadata.git_remotes | yaml }}
62+
{% endif %}

tile_generator/test_config_expected_output.json

+7
Original file line numberDiff line numberDiff line change
@@ -4256,6 +4256,13 @@
42564256
}
42574257
],
42584258
"sha1": false,
4259+
"git_remotes": [
4260+
{
4261+
"name": "origin",
4262+
"urls": ["[email protected]:org/repo.git"]
4263+
}
4264+
],
4265+
"git_sha": "some-sha",
42594266
"space": "test-tile-space",
42604267
"standalone": false,
42614268
"stemcell_criteria": {

tile_generator/tile.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,23 @@ def init_cmd(name):
5454
@click.option('--verbose', is_flag=True)
5555
@click.option('--sha1', is_flag=True)
5656
@click.option('--cache', type=str, default=None)
57-
def build_cmd(version, verbose, sha1, cache):
57+
@click.option('--git-info', type=bool, default=True)
58+
def build_cmd(version, verbose, sha1, cache, git_info):
5859
cfg = Config().read()
5960

6061
cfg.set_version(version)
6162
cfg.set_verbose(verbose)
62-
cfg.set_sha1(sha1)
6363
cfg.set_cache(cache)
64+
cfg.set_git_remotes(git_info)
65+
cfg.set_git_sha(git_info)
66+
cfg.set_sha1(sha1)
6467
print('name:', cfg.get('name', '<unspecified>'))
6568
print('label:', cfg.get('label', '<unspecified>'))
6669
print('description:', cfg.get('description', '<unspecified>'))
6770
print('version:', cfg.get('version', '<unspecified>'))
6871
print('sha1:', cfg.get('sha1', '<unspecified>'))
72+
print('git_remotes:', cfg.get('git_remotes', '<unspecified>'))
73+
print('git_sha:', cfg.get('git_sha', '<unspecified>'))
6974
stemcell = cfg.get('stemcell_criteria', {})
7075
print('stemcell:', stemcell.get('os', '<unspecified>'), stemcell.get('version', '<unspecified>'))
7176
print()

tile_generator/tile_metadata.py

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(self, config):
1818
def build(self):
1919
self._build_base()
2020
self._build_stemcell_criteria()
21+
self._build_git_info()
2122
self._build_property_blueprints()
2223
self._build_form_types()
2324
self._build_job_types()
@@ -46,6 +47,10 @@ def _build_base(self):
4647

4748
self.tile_metadata['base'] = base
4849

50+
def _build_git_info(self):
51+
self.tile_metadata['git_remotes'] = self.config.get('git_remotes')
52+
self.tile_metadata['git_sha'] = self.config.get('git_sha')
53+
4954
def _build_stemcell_criteria(self):
5055
stemcell_criteria = self.config['stemcell_criteria']
5156
# Note: tile.py uses self.config['stemcell_criteria']

tile_generator/tile_metadata_unittest.py

+14
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,19 @@ def test_non_standalone_tile_includes_default_cf_blueprints(self):
145145

146146
self.assertEqual(metadata.tile_metadata['property_blueprints'], expected_blueprints)
147147

148+
def test_git_info(self):
149+
config = Config(
150+
name='validname',
151+
git_sha='some-sha',
152+
git_remotes=[{'name': 'name', 'sha1': 'abc'}]
153+
)
154+
155+
metadata = TileMetadata(config)
156+
metadata._build_git_info()
157+
158+
self.assertEqual(metadata.tile_metadata['git_sha'], 'some-sha')
159+
self.assertEqual(metadata.tile_metadata['git_remotes'], [{'name': 'name', 'sha1': 'abc'}])
160+
161+
148162
if __name__ == '__main__':
149163
unittest.main()

0 commit comments

Comments
 (0)