GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Test Failed
Push — develop-v1.6.0 ( 9d5181...7efb31 )
by
unknown
04:49
created

DownloadGitRepoAction   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 251
rs 7.4757
c 0
b 0
f 0
wmc 53

13 Methods

Rating   Name   Duplication   Size   Complexity  
B _is_desired_pack() 0 17 5
A __init__() 0 4 1
A _cleanup_repo() 0 5 2
C run() 0 38 7
B _eval_repo_url() 0 11 5
B _tag_pack() 0 22 5
A _clone_repo() 0 14 2
A _eval_repo_name() 0 15 2
B _apply_pack_permissions() 0 20 5
B _validate_result() 0 25 5
A _eval_subtree() 0 9 3
B _lookup_cached_gitinfo() 0 23 4
C _move_packs() 0 35 7

How to fix   Complexity   

Complex Class

Complex classes like DownloadGitRepoAction often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
import shutil
18
import hashlib
19
import json
20
import stat
21
22
import six
23
from git.repo import Repo
24
from lockfile import LockFile
25
26
from st2actions.runners.pythonrunner import Action
27
from st2common.util.green import shell
28
29
ALL_PACKS = '*'
30
PACK_REPO_ROOT = 'packs'
31
MANIFEST_FILE = 'pack.yaml'
32
CONFIG_FILE = 'config.yaml'
33
GITINFO_FILE = '.gitinfo'
34
PACK_RESERVE_CHARACTER = '.'
35
36
STACKSTORM_CONTRIB_REPOS = [
37
    'st2contrib',
38
    'st2incubator'
39
]
40
41
#####
42
# !!!!!!!!!!!!!!
43
# !!! README !!!
44
# !!!!!!!!!!!!!!
45
#
46
# This NEEDS a rewrite. Too many features and far too many assumption
47
# to keep this impl straight any longer. If you only want to read code do
48
# so at your own peril.
49
#
50
# If you are here to fix a bug or add a feature answer these questions -
51
# 1. Am I fixing a broken feature?
52
# 2. Is this the only module in which to fix the bug?
53
# 3. Am I sure this is a bug fix and not a feature?
54
#
55
# Only if you can emphatically answer 'YES' to allow about questions you should
56
# touch this file. Else, be warned you might loose a part of you soul or sanity.
57
#####
58
59
60
PACK_GROUP_CFG_KEY = 'pack_group'
61
62
63
class DownloadGitRepoAction(Action):
64
    def __init__(self, config=None, action_service=None):
65
        super(DownloadGitRepoAction, self).__init__(config=config, action_service=action_service)
66
        self._subtree = None
67
        self._repo_url = None
68
69
    def run(self, packs, repo_url, abs_repo_base, verifyssl=True, branch='master', subtree=False):
70
71
        cached_repo_url, cached_branch, cached_subtree = self._lookup_cached_gitinfo(
72
            abs_repo_base, packs)
73
74
        if not repo_url:
75
            repo_url = cached_repo_url
76
        if not branch:
77
            branch = cached_branch
78
        # Making the assumption that is no repo_url change was required
79
        # the subtree nature should be inferred from cached value.
80
        if repo_url == cached_repo_url:
81
            subtree = cached_subtree
82
83
        self._subtree = self._eval_subtree(repo_url, subtree)
84
        self._repo_url = self._eval_repo_url(repo_url)
85
86
        repo_name = self._eval_repo_name(self._repo_url)
87
        lock_name = hashlib.md5(repo_name).hexdigest() + '.lock'
88
89
        with LockFile('/tmp/%s' % (lock_name)):
90
            abs_local_path = self._clone_repo(repo_url=self._repo_url, verifyssl=verifyssl,
91
                                              branch=branch)
92
            try:
93
                if self._subtree:
94
                    # st2-contrib repo has a top-level packs folder that actually contains the
95
                    pack_abs_local_path = os.path.join(abs_local_path, PACK_REPO_ROOT)
96
                    # resolve ALL_PACK here to avoid wild-cards
97
                    if ALL_PACKS in packs:
98
                        packs = os.listdir(pack_abs_local_path)
99
                else:
100
                    pack_abs_local_path = abs_local_path
101
102
                self._tag_pack(pack_abs_local_path, packs, self._subtree)
103
                result = self._move_packs(abs_repo_base, packs, pack_abs_local_path, self._subtree)
104
            finally:
105
                self._cleanup_repo(abs_local_path)
106
        return self._validate_result(result=result, packs=packs, repo_url=self._repo_url)
107
108
    @staticmethod
109
    def _clone_repo(repo_url, verifyssl=True, branch='master'):
110
        user_home = os.path.expanduser('~')
111
        # Assuming git url is of form [email protected]:user/git-repo.git
112
        repo_name = DownloadGitRepoAction._eval_repo_name(repo_url)
113
        abs_local_path = os.path.join(user_home, repo_name)
114
115
        # Disable SSL cert checking if explictly asked
116
        if not verifyssl:
117
            os.environ['GIT_SSL_NO_VERIFY'] = 'true'
118
        # Shallow clone the repo to avoid getting all the metadata. We only need HEAD of a
119
        # specific branch so save some download time.
120
        Repo.clone_from(repo_url, abs_local_path, branch=branch, depth=1)
121
        return abs_local_path
122
123
    def _move_packs(self, abs_repo_base, packs, abs_local_path, subtree):
124
        result = {}
125
126
        for pack in packs:
127
            if subtree:
128
                abs_pack_temp_location = os.path.join(abs_local_path, pack)
129
            else:
130
                abs_pack_temp_location = abs_local_path
131
132
            desired, message = DownloadGitRepoAction._is_desired_pack(abs_pack_temp_location, pack)
133
            if desired:
134
                to = abs_repo_base
135
                dest_pack_path = os.path.join(abs_repo_base, pack)
136
                if os.path.exists(dest_pack_path):
137
                    self.logger.debug('Removing existing pack %s in %s to replace.', pack,
138
                                      dest_pack_path)
139
140
                    # Ensure to preserve any existing configuration
141
                    old_config_file = os.path.join(dest_pack_path, CONFIG_FILE)
142
                    new_config_file = os.path.join(abs_pack_temp_location, CONFIG_FILE)
143
144
                    if os.path.isfile(old_config_file):
145
                        shutil.move(old_config_file, new_config_file)
146
147
                    shutil.rmtree(dest_pack_path)
148
149
                self.logger.debug('Moving pack from %s to %s.', abs_pack_temp_location, to)
150
                shutil.move(abs_pack_temp_location, to)
151
                # post move fix all permissions.
152
                self._apply_pack_permissions(pack_path=dest_pack_path)
153
                message = 'Success.'
154
            elif message:
155
                message = 'Failure : %s' % message
156
            result[pack] = (desired, message)
157
        return result
158
159
    def _apply_pack_permissions(self, pack_path):
160
        """
161
        Will recursively apply permission 770 to pack and its contents.
162
        """
163
        # 1. switch owner group to configuered group
164
        pack_group = self.config.get(PACK_GROUP_CFG_KEY, None)
165
        if pack_group:
166
            shell.run_command(['sudo', 'chgrp', '-R', pack_group, pack_path])
167
168
        # 2. Setup the right permissions and group ownership
169
        # These mask is same as mode = 775
170
        mode = stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH
171
        os.chmod(pack_path, mode)
172
173
        # Yuck! Since os.chmod does not support chmod -R walk manually.
174
        for root, dirs, files in os.walk(pack_path):
175
            for d in dirs:
176
                os.chmod(os.path.join(root, d), mode)
177
            for f in files:
178
                os.chmod(os.path.join(root, f), mode)
179
180
    @staticmethod
181
    def _is_desired_pack(abs_pack_path, pack_name):
182
        # path has to exists.
183
        if not os.path.exists(abs_pack_path):
184
            return (False, 'Pack "%s" not found or it\'s missing a "pack.yaml" file.' %
185
                    (pack_name))
186
        # Must be a dir.
187
        if not os.path.isdir(abs_pack_path):
188
            return (False, '%s is not a expected directory structure.' % (pack_name))
189
        # should not include reserve characters
190
        if PACK_RESERVE_CHARACTER in pack_name:
191
            return (False, 'Pack name "%s" contains reserve character "%s"' %
192
                    (pack_name, PACK_RESERVE_CHARACTER))
193
        # must contain a manifest file. Empty file is ok for now.
194
        if not os.path.isfile(os.path.join(abs_pack_path, MANIFEST_FILE)):
195
            return (False, 'Pack is missing a manifest file (%s).' % (MANIFEST_FILE))
196
        return (True, '')
197
198
    @staticmethod
199
    def _cleanup_repo(abs_local_path):
200
        # basic lock checking etc?
201
        if os.path.isdir(abs_local_path):
202
            shutil.rmtree(abs_local_path)
203
204
    @staticmethod
205
    def _validate_result(result, packs, repo_url):
206
        atleast_one_success = False
207
        sanitized_result = {}
208
        for k, v in six.iteritems(result):
209
            atleast_one_success |= v[0]
210
            sanitized_result[k] = v[1]
211
212
        if not atleast_one_success:
213
            message_list = []
214
            message_list.append('No packs were downloaded from repository "%s".\n' % (repo_url))
215
            message_list.append('Errors:')
216
217
            for pack, value in result.items():
218
                success, error = value
219
220
                if success:
221
                    continue
222
223
                message_list.append(' - %s: %s' % (pack, error))
224
225
            message = '\n'.join(message_list)
226
            raise Exception(message)
227
228
        return sanitized_result
229
230
    @staticmethod
231
    def _eval_subtree(repo_url, subtree):
232
        match = False
233
        for stackstorm_repo_name in STACKSTORM_CONTRIB_REPOS:
234
            if stackstorm_repo_name in repo_url:
235
                match = True
236
                break
237
238
        return subtree | match
239
240
    @staticmethod
241
    def _eval_repo_url(repo_url):
242
        """Allow passing short GitHub style URLs"""
243
        if not repo_url:
244
            raise Exception('No valid reo_url provided or could be inferred.')
245
        has_git_extension = repo_url.endswith('.git')
246
        if len(repo_url.split('/')) == 2 and "git@" not in repo_url:
247
            url = "https://github.com/{}".format(repo_url)
248
        else:
249
            url = repo_url
250
        return url if has_git_extension else "{}.git".format(url)
251
252
    @staticmethod
253
    def _lookup_cached_gitinfo(abs_repo_base, packs):
254
        """
255
        This method will try to lookup the repo_url from the first pack in the list
256
        of packs. It works under some strict assumptions -
257
        1. repo_url was not originally specified
258
        2. all packs from from same repo
259
        3. gitinfo was originally added by this action
260
        """
261
        repo_url = None
262
        branch = None
263
        subtree = False
264
        if len(packs) < 1:
265
            raise Exception('No packs specified.')
266
        gitinfo_location = os.path.join(abs_repo_base, packs[0], GITINFO_FILE)
267
        if not os.path.exists(gitinfo_location):
268
            return repo_url, branch, subtree
269
        with open(gitinfo_location, 'r') as gitinfo_fp:
270
            gitinfo = json.load(gitinfo_fp)
271
            repo_url = gitinfo.get('repo_url', None)
272
            branch = gitinfo.get('branch', None)
273
            subtree = gitinfo.get('subtree', False)
274
        return repo_url, branch, subtree
275
276
    @staticmethod
277
    def _eval_repo_name(repo_url):
278
        """
279
        Evaluate the name of the repo.
280
        https://github.com/StackStorm/st2contrib.git -> st2contrib
281
        https://github.com/StackStorm/st2contrib -> st2contrib
282
        [email protected]:StackStorm/st2contrib.git -> st2contrib
283
        [email protected]:StackStorm/st2contrib -> st2contrib
284
        """
285
        last_forward_slash = repo_url.rfind('/')
286
        next_dot = repo_url.find('.', last_forward_slash)
287
        # If dot does not follow last_forward_slash return till the end
288
        if next_dot < last_forward_slash:
289
            return repo_url[last_forward_slash + 1:]
290
        return repo_url[last_forward_slash + 1:next_dot]
291
292
    def _tag_pack(self, pack_root, packs, subtree):
293
        """Add git information to pack directory for retrieval later"""
294
295
        repo = Repo(pack_root)
296
        payload = {
297
            'repo_url': repo.remotes[0].url,
298
            'branch': repo.active_branch.name,
299
            'ref': repo.head.commit.hexsha,
300
            'subtree': subtree
301
        }
302
303
        for pack in packs:
304
            pack_dir = os.path.join(pack_root, pack) if subtree else pack_root
305
306
            if not os.path.exists(pack_dir):
307
                self.logger.warn('%s is missing. Expected location "%s".', pack, pack_dir)
308
                continue
309
310
            info_file = os.path.join(pack_dir, GITINFO_FILE)
311
312
            with open(info_file, "w") as gitinfo:
313
                gitinfo.write(json.dumps(payload))
314