Different variables for API endpoint and svn checkout URL.
[pillar-svnman.git] / svnman / __init__.py
1 import logging
2 import os.path
3 from urllib.parse import urljoin
4
5 import flask
6 from werkzeug.local import LocalProxy
7
8 import pillarsdk
9 from pillar.extension import PillarExtension
10 from pillar.auth import current_user
11 from pillar.api.projects import utils as proj_utils
12 from pillar import current_app
13
14 EXTENSION_NAME = 'svnman'
15
16
17 # SVNman stores the following keys in the project extension properties:
18 # repo_id: the Subversion repository ID
19 # users: list of ObjectIDs of users having access to the project
20
21 class SVNManExtension(PillarExtension):
22     user_caps = {
23         'subscriber-pro': frozenset({'svn-use'}),
24         'demo': frozenset({'svn-use'}),
25         'admin': frozenset({'svn-use', 'svn-admin'}),
26     }
27
28     def __init__(self):
29         from . import remote
30
31         self._log = logging.getLogger('%s.SVNManExtension' % __name__)
32         self.remote: remote.API = None
33
34     @property
35     def name(self):
36         return EXTENSION_NAME
37
38     def flask_config(self):
39         """Returns extension-specific defaults for the Flask configuration.
40
41         Use this to set sensible default values for configuration settings
42         introduced by the extension.
43
44         :rtype: dict
45         """
46
47         # Just so that it registers the management commands.
48         from . import cli
49
50         return {
51             'SVNMAN_REPO_URL': 'http://SVNMAN_REPO_URL/repo/',
52             'SVNMAN_API_URL': 'http://SVNMAN_API_URL/api/',
53             'SVNMAN_API_USERNAME': 'SVNMAN_API_USERNAME',
54             'SVNMAN_API_PASSWORD': 'SVNMAN_API_PASSWORD',
55         }
56
57     def eve_settings(self):
58         """Returns extensions to the Eve settings.
59
60         Currently only the DOMAIN key is used to insert new resources into
61         Eve's configuration.
62
63         :rtype: dict
64         """
65         return {'DOMAIN': {}}
66
67     def blueprints(self):
68         """Returns the list of top-level blueprints for the extension.
69
70         These blueprints will be mounted at the url prefix given to
71         app.load_extension().
72
73         :rtype: list of flask.Blueprint objects.
74         """
75
76         from . import routes
77
78         return [
79             routes.blueprint,
80         ]
81
82     def setup_app(self, app):
83         from . import remote
84
85         self.remote = remote.API(
86             remote_url=app.config['SVNMAN_API_URL'],
87             username=app.config['SVNMAN_API_USERNAME'],
88             password=app.config['SVNMAN_API_PASSWORD'],
89         )
90
91     @property
92     def template_path(self):
93         return os.path.join(os.path.dirname(__file__), 'templates')
94
95     @property
96     def static_path(self):
97         return os.path.join(os.path.dirname(__file__), 'static')
98
99     def sidebar_links(self, project):
100         if not current_user.has_cap('svn-use'):
101             return ''
102         return flask.render_template('svnman/sidebar.html', project=project)
103
104     @property
105     def has_project_settings(self) -> bool:
106         return current_user.has_cap('svn-use')
107
108     def project_settings(self, project: pillarsdk.Project, **template_args: dict) -> flask.Response:
109         """Renders the project settings page for this extension.
110
111         Set YourExtension.has_project_settings = True and Pillar will call this function.
112
113         :param project: the project for which to render the settings.
114         :param template_args: additional template arguments.
115         :returns: a Flask HTTP response
116         """
117
118         from .routes import project_settings
119
120         remote_url = current_app.config['SVNMAN_REPO_URL']
121
122         if self.is_svnman_project(project):
123             repo_id = project.extension_props[EXTENSION_NAME].repo_id
124             svn_url = urljoin(remote_url, repo_id)
125         else:
126             svn_url = ''
127             repo_id = ''
128
129         return project_settings(project,
130                                 svn_url=svn_url,
131                                 repo_id=repo_id,
132                                 remote_url=remote_url,
133                                 **template_args)
134
135     def is_svnman_project(self, project: pillarsdk.Project) -> bool:
136         """Checks whether the project is correctly set up for SVNman."""
137
138         if not project.extension_props:
139             return False
140
141         try:
142             pprops = project.extension_props[EXTENSION_NAME]
143         except AttributeError:
144             self._log.warning("is_svnman_project: Project url=%r doesn't have"
145                               " any extension properties.", project['url'])
146             if self._log.isEnabledFor(logging.DEBUG):
147                 import pprint
148                 self._log.debug('Project: %s', pprint.pformat(project.to_dict()))
149             return False
150         except KeyError:
151             return False
152
153         if pprops is None:
154             self._log.warning("is_svnman_project: Project url=%r doesn't have"
155                               " Flamenco extension properties.", project['url'])
156             return False
157
158         return bool(pprops.repo_id)
159
160     def create_repo(self, project_url: str, creator: str) -> str:
161         """Creates a SVN repository with a random ID attached to the project.
162
163         Saves the repository ID in the project. Is a no-op if the project
164         already has a Subversion repository.
165         """
166
167         import random
168         import string
169
170         from . import remote, exceptions
171
172         alphabet = string.ascii_letters + string.digits
173
174         proj = proj_utils.get_project(project_url)
175         project_id = proj['_id']
176         eprops = proj.setdefault('extension_props', {}).setdefault(EXTENSION_NAME, {})
177
178         repo_id = eprops.get('repo_id')
179         if repo_id:
180             self._log.warning('project %s already has a Subversion repository %r',
181                               project_id, repo_id)
182             return repo_id
183
184         def random_id():
185             return ''.join([random.choice(alphabet) for _ in range(24)])
186
187         repo_info = remote.CreateRepo(
188             repo_id='',
189             project_id=str(project_id),
190             creator=creator,
191         )
192
193         for _ in range(100):
194             repo_info.repo_id = random_id()
195             self._log.info('creating new repository, trying out %s', repo_info)
196             try:
197                 self.remote.create_repo(repo_info)
198             except exceptions.RepoAlreadyExists:
199                 self._log.info('repo_id=%r already exists, trying random other one',
200                                repo_info.repo_id)
201             else:
202                 break
203         else:
204             self._log.error('unable to find unique random repository ID, giving up')
205             raise ValueError('unable to find unique random repository ID, giving up')
206
207         self._log.info('created new Subversion repository: %s', repo_info)
208
209         # Update the project to include the repository ID.
210         eprops['repo_id'] = repo_info.repo_id
211         proj_utils.put_project(proj)
212
213         return repo_info.repo_id
214
215     def delete_repo(self, project_url: str, repo_id: str):
216         """Deletes an SVN repository and detaches it from the project."""
217
218         from . import remote, exceptions
219
220         proj = proj_utils.get_project(project_url)
221         project_id = proj['_id']
222         eprops = proj.setdefault('extension_props', {}).setdefault(EXTENSION_NAME, {})
223
224         proj_repo_id = eprops.get('repo_id')
225         if proj_repo_id != repo_id:
226             self._log.warning('project %s is linked to repo %r, not to %r, refusing to delete',
227                               project_id, proj_repo_id, repo_id)
228             raise ValueError()
229
230         self.remote.delete_repo(repo_id)
231         self._log.info('deleted Subversion repository %s', repo_id)
232
233         # Update the project to remove the repository ID.
234         eprops.pop('repo_id', None)
235         eprops.pop('access', None)
236         proj_utils.put_project(proj)
237
238
239 def _get_current_svnman() -> SVNManExtension:
240     """Returns the SVNMan extension of the current application."""
241
242     return flask.current_app.pillar_extensions[EXTENSION_NAME]
243
244
245 current_svnman: SVNManExtension = LocalProxy(_get_current_svnman)
246 """SVNMan extension of the current app."""