Skip to content

Commit d47e267

Browse files
committed
Pack dependency management
Fixes https://github.com/StackStorm/discussions/issues/354 1. `packs.install` workflow changed from `achtion-chain` to `orquesta` for workflow complexity. 2. Download all packs and dependency packs, check for conflict packs before `install pack requirements` and `register pack`. 3. Workflow fails on any time if conflict pack is recognized, downloaded packs will be removed. 4. Register packs in reverse order, it means the last dependency pack will be installed first. 5. New option `--skip` (default value is False) for skipping dependencies check: `st2 install pack --skip` 6. Only case that will check conflict is that if pack was installed and dependency pack has specified pack version. 7. Nested level of dependencies to prevent infinite or long download loops is 3. 8. Support StackStorm Exchange packs, the private repo ULR and local file system for conflict pack checking.
1 parent 544120a commit d47e267

File tree

17 files changed

+351
-60
lines changed

17 files changed

+351
-60
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Added
1010
* Add support for blacklisting / whitelisting hosts to the HTTP runner by adding new
1111
``url_hosts_blacklist`` and ``url_hosts_whitelist`` runner attribute. (new feature)
1212
#4757
13+
* Add support for Pack dependency management to install dependency packs and find out confilct packs.
14+
#4769
1315

1416
Changed
1517
~~~~~~~

contrib/packs/actions/delete.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@
1414
type: "string"
1515
default: "/opt/stackstorm/packs/"
1616
immutable: true
17+
delete_env:
18+
type: "boolean"
19+
description: Flag for if need to delete virtual environment.
20+
default: true
21+
required: false

contrib/packs/actions/download.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,9 @@
2727
description: "True to use Python 3 binary for this pack."
2828
required: false
2929
default: false
30+
dependency_list:
31+
type: "array"
32+
description: "Dependency list that needs to be downloaded."
33+
items:
34+
type: "string"
35+
required: false
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
3+
---
4+
name: "get_pack_dependencies"
5+
runner_type: "python-script"
6+
description: "Get pack dependencies specified in pack.yaml"
7+
enabled: true
8+
pack: packs
9+
entry_point: "pack_mgmt/get_pack_dependencies.py"
10+
parameters:
11+
packs_status:
12+
type: object
13+
description: Name of the pack in Exchange or a git repo URL and download status.
14+
required: true
15+
default: null
16+
nested:
17+
type: integer
18+
description: Nested level of dependencies to prevent infinite or really long download loops.
19+
required: true
20+
default: 3

contrib/packs/actions/install.meta.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: "install"
3-
runner_type: "action-chain"
3+
runner_type: "orquesta"
44
description: "Installs or upgrades a pack into local content repository, either by
55
git URL or a short name matching an index entry.
66
Will download pack, load the actions, sensors and rules from the pack.
@@ -32,3 +32,8 @@
3232
description: "Use Python 3 binary when creating a virtualenv for this pack."
3333
required: false
3434
default: false
35+
skip:
36+
type: "boolean"
37+
description: "Set to True to skip pack dependency installations."
38+
required: false
39+
default: false

contrib/packs/actions/pack_mgmt/delete.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self, config=None, action_service=None):
3030
self._base_virtualenvs_path = os.path.join(cfg.CONF.system.base_path,
3131
'virtualenvs/')
3232

33-
def run(self, packs, abs_repo_base):
33+
def run(self, packs, abs_repo_base, delete_env=True):
3434
intersection = BLOCKED_PACKS & frozenset(packs)
3535
if len(intersection) > 0:
3636
names = ', '.join(list(intersection))
@@ -43,12 +43,13 @@ def run(self, packs, abs_repo_base):
4343
self.logger.debug('Deleting pack directory "%s"' % (abs_fp))
4444
shutil.rmtree(abs_fp)
4545

46-
# 2. Delete pack virtual environment
47-
for pack_name in packs:
48-
pack_name = quote_unix(pack_name)
49-
virtualenv_path = os.path.join(self._base_virtualenvs_path, pack_name)
46+
if delete_env:
47+
# 2. Delete pack virtual environment
48+
for pack_name in packs:
49+
pack_name = quote_unix(pack_name)
50+
virtualenv_path = os.path.join(self._base_virtualenvs_path, pack_name)
5051

51-
if os.path.isdir(virtualenv_path):
52-
self.logger.debug('Deleting virtualenv "%s" for pack "%s"' %
53-
(virtualenv_path, pack_name))
54-
shutil.rmtree(virtualenv_path)
52+
if os.path.isdir(virtualenv_path):
53+
self.logger.debug('Deleting virtualenv "%s" for pack "%s"' %
54+
(virtualenv_path, pack_name))
55+
shutil.rmtree(virtualenv_path)

contrib/packs/actions/pack_mgmt/download.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,29 @@ def __init__(self, config=None, action_service=None):
6262
if self.proxy_ca_bundle_path and not os.environ.get('proxy_ca_bundle_path', None):
6363
os.environ['no_proxy'] = self.no_proxy
6464

65-
def run(self, packs, abs_repo_base, verifyssl=True, force=False, python3=False):
65+
def run(self, packs, abs_repo_base, verifyssl=True, force=False, python3=False,
66+
dependency_list=None):
6667
result = {}
67-
68-
for pack in packs:
69-
pack_result = download_pack(pack=pack, abs_repo_base=abs_repo_base,
70-
verify_ssl=verifyssl, force=force,
71-
proxy_config=self.proxy_config,
72-
force_permissions=True,
73-
use_python3=python3,
74-
logger=self.logger)
75-
pack_url, pack_ref, pack_result = pack_result
76-
result[pack_ref] = pack_result
68+
pack_url = None
69+
70+
if dependency_list:
71+
for pack_dependency in dependency_list:
72+
pack_result = download_pack(pack=pack_dependency, abs_repo_base=abs_repo_base,
73+
verify_ssl=verifyssl, force=force,
74+
proxy_config=self.proxy_config, force_permissions=True,
75+
use_python3=python3, logger=self.logger)
76+
pack_url, pack_ref, pack_result = pack_result
77+
result[pack_ref] = pack_result
78+
else:
79+
for pack in packs:
80+
pack_result = download_pack(pack=pack, abs_repo_base=abs_repo_base,
81+
verify_ssl=verifyssl, force=force,
82+
proxy_config=self.proxy_config,
83+
force_permissions=True,
84+
use_python3=python3,
85+
logger=self.logger)
86+
pack_url, pack_ref, pack_result = pack_result
87+
result[pack_ref] = pack_result
7788

7889
return self._validate_result(result=result, repo_url=pack_url)
7990

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright 2019 Extreme Networks, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import six
16+
17+
from st2common.constants.pack import PACK_VERSION_SEPARATOR
18+
from st2common.content.utils import get_pack_base_path
19+
from st2common.runners.base_action import Action
20+
from st2common.util.pack import get_pack_metadata
21+
22+
STACKSTORM_EXCHANGE_PACK = 'StackStorm-Exchange/stackstorm-'
23+
LOCAL_FILE_PREFIX = 'file://'
24+
25+
26+
class GetPackDependencies(Action):
27+
def __init__(self, config=None, action_service=None):
28+
self.dependency_list = []
29+
self.conflict_list = []
30+
31+
def run(self, packs_status, nested):
32+
"""
33+
:param packs_status: Name of the pack in Exchange or a git repo URL and download status.
34+
:type: packs_status: ``dict``
35+
36+
:param nested: Nested level of dependencies to prevent infinite or really
37+
long download loops.
38+
:type nested: ``integer``
39+
"""
40+
result = {}
41+
42+
if not packs_status or nested == 0:
43+
return result
44+
45+
for pack, status in six.iteritems(packs_status):
46+
if 'success' not in status.lower():
47+
continue
48+
49+
dependency_packs = get_dependency_list(pack)
50+
if not dependency_packs:
51+
continue
52+
53+
for dependency_pack in dependency_packs:
54+
pack_and_version = dependency_pack.split(PACK_VERSION_SEPARATOR)
55+
name_or_url = pack_and_version[0]
56+
pack_version = pack_and_version[1] if len(pack_and_version) > 1 else None
57+
58+
if name_or_url.startswith(LOCAL_FILE_PREFIX):
59+
self.get_local_pack_dependencies(name_or_url, pack_version, dependency_pack)
60+
elif (len(name_or_url.split('/')) == 1 and STACKSTORM_EXCHANGE_PACK not in
61+
name_or_url) or STACKSTORM_EXCHANGE_PACK in name_or_url:
62+
self.get_exchange_pack_dependencies(name_or_url, pack_version, dependency_pack)
63+
else:
64+
self.get_none_exchange_pack_dependencies(name_or_url, pack_version,
65+
dependency_pack)
66+
67+
result['dependency_list'] = self.dependency_list
68+
result['conflict_list'] = self.conflict_list
69+
result['nested'] = nested - 1
70+
71+
return result
72+
73+
# For StackStorm Exchange packs. E.g.
74+
# email
75+
# https://github.com/StackStorm-Exchange/stackstorm-email
76+
# https://github.com/StackStorm-Exchange/stackstorm-email.git
77+
def get_exchange_pack_dependencies(self, name_or_url, pack_version, dependency_pack):
78+
if len(name_or_url.split('/')) == 1:
79+
pack_name = name_or_url
80+
else:
81+
name_or_git = name_or_url.split(STACKSTORM_EXCHANGE_PACK)[-1]
82+
pack_name = name_or_git if '.git' not in name_or_git else name_or_git.split('.')[0]
83+
84+
existing_pack_version = get_pack_version(pack_name)
85+
self.check_conflict(dependency_pack, pack_version, existing_pack_version)
86+
87+
# For None StackStorm Exchange packs. E.g.
88+
# https://github.com/EncoreTechnologies/stackstorm-freeipa.git
89+
# https://github.com/EncoreTechnologies/stackstorm-freeipa
90+
# https://github.com/EncoreTechnologies/pack.git
91+
def get_none_exchange_pack_dependencies(self, name_or_url, pack_version, dependency_pack):
92+
name_or_git = name_or_url.split('/')[-1]
93+
name = name_or_git if '.git' not in name_or_git else name_or_git.split('.')[0]
94+
pack_name = name.split('-')[-1] if "stackstorm-" in name else name
95+
96+
existing_pack_version = get_pack_version(pack_name)
97+
self.check_conflict(dependency_pack, pack_version, existing_pack_version)
98+
99+
# For local file. E.g
100+
# file:///opt/stackstorm/st2/lib/python3.6/site-packages/st2tests/fixtures/packs/dummy_pack_3
101+
def get_local_pack_dependencies(self, name_or_url, pack_version, dependency_pack):
102+
pack_name = name_or_url.split("/")[-1]
103+
104+
existing_pack_version = get_pack_version(pack_name)
105+
self.check_conflict(dependency_pack, pack_version, existing_pack_version)
106+
107+
def check_conflict(self, pack, version, existing_version):
108+
if existing_version:
109+
existing_version = 'v' + existing_version
110+
if version and existing_version != version and pack not in self.conflict_list:
111+
self.conflict_list.append(pack)
112+
elif pack not in self.dependency_list:
113+
self.dependency_list.append(pack)
114+
115+
116+
def get_pack_version(pack=None):
117+
pack_path = get_pack_base_path(pack)
118+
try:
119+
pack_metadata = get_pack_metadata(pack_dir=pack_path)
120+
return pack_metadata.get('version', None)
121+
except Exception:
122+
return None
123+
124+
125+
def get_dependency_list(pack=None):
126+
pack_path = get_pack_base_path(pack)
127+
128+
try:
129+
pack_metadata = get_pack_metadata(pack_dir=pack_path)
130+
return pack_metadata.get('dependencies', None)
131+
except Exception:
132+
print ('Could not open pack.yaml at location %s' % pack_path)
133+
return None

contrib/packs/actions/pack_mgmt/register.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def run(self, register, packs=None):
7272
'types': types
7373
}
7474

75+
packs.reverse()
7576
if packs:
7677
method_kwargs['packs'] = packs
7778

contrib/packs/actions/pack_mgmt/virtualenv_setup_prerun.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,22 @@
1818

1919

2020
class PacksTransformationAction(Action):
21-
def run(self, packs_status):
21+
def run(self, packs_status, packs_list=None):
2222
"""
2323
:param packs_status: Result from packs.download action.
2424
:type: packs_status: ``dict``
25+
26+
:param packs_list: Names of the pack in Exchange, a git repo URL or local file system.
27+
:type: packs_list: ``list``
2528
"""
29+
if not packs_list:
30+
packs_list = []
31+
2632
packs = []
2733
for pack_name, status in six.iteritems(packs_status):
2834
if 'success' in status.lower():
2935
packs.append(pack_name)
30-
return packs
36+
37+
packs_list.extend(packs)
38+
39+
return packs_list

0 commit comments

Comments
 (0)