~spiculecharms/gitlab-server

Owner: spicule
Status: Needs Review
Vote: +0 (+2 needed for approval)

CPP?: No
OIL?: No


Tests

Substrate Status Results Last Updated
lxc RETRY 19 days ago
aws RETRY 19 days ago
gce RETRY 19 days ago

Add Comment

Login to comment/vote on this review.


Policy Checklist

Description Unreviewed Pass Fail

General

Must verify that any software installed or utilized is verified as coming from the intended source.
  • Any software installed from the Ubuntu or CentOS default archives satisfies this due to the apt and yum sources including cryptographic signing information.
  • Third party repositories must be listed as a configuration option that can be overridden by the user and not hard coded in the charm itself.
  • Launchpad PPAs are acceptable as the add-apt-repository command retrieves the keys securely.
  • Other third party repositories are acceptable if the signing key is embedded in the charm.
Must provide a means to protect users from known security vulnerabilities in a way consistent with best practices as defined by either operating system policies or upstream documentation.
Basically, this means there must be instructions on how to apply updates if you use software not from distribution channels.
Must have hooks that are idempotent.
Should be built using charm layers.
Should use Juju Resources to deliver required payloads.

Testing and Quality

charm proof must pass without errors or warnings.
Must include passing unit, functional, or integration tests.
Tests must exercise all relations.
Tests must exercise config.
set-config, unset-config, and re-set must be tested as a minimum
Must not use anything infrastructure-provider specific (i.e. querying EC2 metadata service).
Must be self contained unless the charm is a proxy for an existing cloud service, e.g. ec2-elb charm.
Must not use symlinks.
Bundles must only use promulgated charms, they cannot reference charms in personal namespaces.
Must call Juju hook tools (relation-*, unit-*, config-*, etc) without a hard coded path.
Should include a tests.yaml for all integration tests.

Metadata

Must include a full description of what the software does.
Must include a maintainer email address for a team or individual who will be responsive to contact.
Must include a license. Call the file 'copyright' and make sure all files' licenses are specified clearly.
Must be under a Free license.
Must have a well documented and valid README.md.
Must describe the service.
Must describe how it interacts with other services, if applicable.
Must document the interfaces.
Must show how to deploy the charm.
Must define external dependencies, if applicable.
Should link to a recommend production usage bundle and recommended configuration if this differs from the default.
Should reference and link to upstream documentation and best practices.

Security

Must not run any network services using default passwords.
Must verify and validate any external payload
  • Known and understood packaging systems that verify packages like apt, pip, and yum are ok.
  • wget | sh style is not ok.
Should make use of whatever Mandatory Access Control system is provided by the distribution.
Should avoid running services as root.

All changes | Changes since last revision

Source Diff

Inline diff comments 0

No comments yet.

Back to file index

LICENSE

  1
--- 
  2
+++ LICENSE
  3
@@ -0,0 +1,201 @@
  4
+                                 Apache License
  5
+                           Version 2.0, January 2004
  6
+                        http://www.apache.org/licenses/
  7
+
  8
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  9
+
 10
+   1. Definitions.
 11
+
 12
+      "License" shall mean the terms and conditions for use, reproduction,
 13
+      and distribution as defined by Sections 1 through 9 of this document.
 14
+
 15
+      "Licensor" shall mean the copyright owner or entity authorized by
 16
+      the copyright owner that is granting the License.
 17
+
 18
+      "Legal Entity" shall mean the union of the acting entity and all
 19
+      other entities that control, are controlled by, or are under common
 20
+      control with that entity. For the purposes of this definition,
 21
+      "control" means (i) the power, direct or indirect, to cause the
 22
+      direction or management of such entity, whether by contract or
 23
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
 24
+      outstanding shares, or (iii) beneficial ownership of such entity.
 25
+
 26
+      "You" (or "Your") shall mean an individual or Legal Entity
 27
+      exercising permissions granted by this License.
 28
+
 29
+      "Source" form shall mean the preferred form for making modifications,
 30
+      including but not limited to software source code, documentation
 31
+      source, and configuration files.
 32
+
 33
+      "Object" form shall mean any form resulting from mechanical
 34
+      transformation or translation of a Source form, including but
 35
+      not limited to compiled object code, generated documentation,
 36
+      and conversions to other media types.
 37
+
 38
+      "Work" shall mean the work of authorship, whether in Source or
 39
+      Object form, made available under the License, as indicated by a
 40
+      copyright notice that is included in or attached to the work
 41
+      (an example is provided in the Appendix below).
 42
+
 43
+      "Derivative Works" shall mean any work, whether in Source or Object
 44
+      form, that is based on (or derived from) the Work and for which the
 45
+      editorial revisions, annotations, elaborations, or other modifications
 46
+      represent, as a whole, an original work of authorship. For the purposes
 47
+      of this License, Derivative Works shall not include works that remain
 48
+      separable from, or merely link (or bind by name) to the interfaces of,
 49
+      the Work and Derivative Works thereof.
 50
+
 51
+      "Contribution" shall mean any work of authorship, including
 52
+      the original version of the Work and any modifications or additions
 53
+      to that Work or Derivative Works thereof, that is intentionally
 54
+      submitted to Licensor for inclusion in the Work by the copyright owner
 55
+      or by an individual or Legal Entity authorized to submit on behalf of
 56
+      the copyright owner. For the purposes of this definition, "submitted"
 57
+      means any form of electronic, verbal, or written communication sent
 58
+      to the Licensor or its representatives, including but not limited to
 59
+      communication on electronic mailing lists, source code control systems,
 60
+      and issue tracking systems that are managed by, or on behalf of, the
 61
+      Licensor for the purpose of discussing and improving the Work, but
 62
+      excluding communication that is conspicuously marked or otherwise
 63
+      designated in writing by the copyright owner as "Not a Contribution."
 64
+
 65
+      "Contributor" shall mean Licensor and any individual or Legal Entity
 66
+      on behalf of whom a Contribution has been received by Licensor and
 67
+      subsequently incorporated within the Work.
 68
+
 69
+   2. Grant of Copyright License. Subject to the terms and conditions of
 70
+      this License, each Contributor hereby grants to You a perpetual,
 71
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 72
+      copyright license to reproduce, prepare Derivative Works of,
 73
+      publicly display, publicly perform, sublicense, and distribute the
 74
+      Work and such Derivative Works in Source or Object form.
 75
+
 76
+   3. Grant of Patent License. Subject to the terms and conditions of
 77
+      this License, each Contributor hereby grants to You a perpetual,
 78
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 79
+      (except as stated in this section) patent license to make, have made,
 80
+      use, offer to sell, sell, import, and otherwise transfer the Work,
 81
+      where such license applies only to those patent claims licensable
 82
+      by such Contributor that are necessarily infringed by their
 83
+      Contribution(s) alone or by combination of their Contribution(s)
 84
+      with the Work to which such Contribution(s) was submitted. If You
 85
+      institute patent litigation against any entity (including a
 86
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
 87
+      or a Contribution incorporated within the Work constitutes direct
 88
+      or contributory patent infringement, then any patent licenses
 89
+      granted to You under this License for that Work shall terminate
 90
+      as of the date such litigation is filed.
 91
+
 92
+   4. Redistribution. You may reproduce and distribute copies of the
 93
+      Work or Derivative Works thereof in any medium, with or without
 94
+      modifications, and in Source or Object form, provided that You
 95
+      meet the following conditions:
 96
+
 97
+      (a) You must give any other recipients of the Work or
 98
+          Derivative Works a copy of this License; and
 99
+
100
+      (b) You must cause any modified files to carry prominent notices
101
+          stating that You changed the files; and
102
+
103
+      (c) You must retain, in the Source form of any Derivative Works
104
+          that You distribute, all copyright, patent, trademark, and
105
+          attribution notices from the Source form of the Work,
106
+          excluding those notices that do not pertain to any part of
107
+          the Derivative Works; and
108
+
109
+      (d) If the Work includes a "NOTICE" text file as part of its
110
+          distribution, then any Derivative Works that You distribute must
111
+          include a readable copy of the attribution notices contained
112
+          within such NOTICE file, excluding those notices that do not
113
+          pertain to any part of the Derivative Works, in at least one
114
+          of the following places: within a NOTICE text file distributed
115
+          as part of the Derivative Works; within the Source form or
116
+          documentation, if provided along with the Derivative Works; or,
117
+          within a display generated by the Derivative Works, if and
118
+          wherever such third-party notices normally appear. The contents
119
+          of the NOTICE file are for informational purposes only and
120
+          do not modify the License. You may add Your own attribution
121
+          notices within Derivative Works that You distribute, alongside
122
+          or as an addendum to the NOTICE text from the Work, provided
123
+          that such additional attribution notices cannot be construed
124
+          as modifying the License.
125
+
126
+      You may add Your own copyright statement to Your modifications and
127
+      may provide additional or different license terms and conditions
128
+      for use, reproduction, or distribution of Your modifications, or
129
+      for any such Derivative Works as a whole, provided Your use,
130
+      reproduction, and distribution of the Work otherwise complies with
131
+      the conditions stated in this License.
132
+
133
+   5. Submission of Contributions. Unless You explicitly state otherwise,
134
+      any Contribution intentionally submitted for inclusion in the Work
135
+      by You to the Licensor shall be under the terms and conditions of
136
+      this License, without any additional terms or conditions.
137
+      Notwithstanding the above, nothing herein shall supersede or modify
138
+      the terms of any separate license agreement you may have executed
139
+      with Licensor regarding such Contributions.
140
+
141
+   6. Trademarks. This License does not grant permission to use the trade
142
+      names, trademarks, service marks, or product names of the Licensor,
143
+      except as required for reasonable and customary use in describing the
144
+      origin of the Work and reproducing the content of the NOTICE file.
145
+
146
+   7. Disclaimer of Warranty. Unless required by applicable law or
147
+      agreed to in writing, Licensor provides the Work (and each
148
+      Contributor provides its Contributions) on an "AS IS" BASIS,
149
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
150
+      implied, including, without limitation, any warranties or conditions
151
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
152
+      PARTICULAR PURPOSE. You are solely responsible for determining the
153
+      appropriateness of using or redistributing the Work and assume any
154
+      risks associated with Your exercise of permissions under this License.
155
+
156
+   8. Limitation of Liability. In no event and under no legal theory,
157
+      whether in tort (including negligence), contract, or otherwise,
158
+      unless required by applicable law (such as deliberate and grossly
159
+      negligent acts) or agreed to in writing, shall any Contributor be
160
+      liable to You for damages, including any direct, indirect, special,
161
+      incidental, or consequential damages of any character arising as a
162
+      result of this License or out of the use or inability to use the
163
+      Work (including but not limited to damages for loss of goodwill,
164
+      work stoppage, computer failure or malfunction, or any and all
165
+      other commercial damages or losses), even if such Contributor
166
+      has been advised of the possibility of such damages.
167
+
168
+   9. Accepting Warranty or Additional Liability. While redistributing
169
+      the Work or Derivative Works thereof, You may choose to offer,
170
+      and charge a fee for, acceptance of support, warranty, indemnity,
171
+      or other liability obligations and/or rights consistent with this
172
+      License. However, in accepting such obligations, You may act only
173
+      on Your own behalf and on Your sole responsibility, not on behalf
174
+      of any other Contributor, and only if You agree to indemnify,
175
+      defend, and hold each Contributor harmless for any liability
176
+      incurred by, or claims asserted against, such Contributor by reason
177
+      of your accepting any such warranty or additional liability.
178
+
179
+   END OF TERMS AND CONDITIONS
180
+
181
+   APPENDIX: How to apply the Apache License to your work.
182
+
183
+      To apply the Apache License to your work, attach the following
184
+      boilerplate notice, with the fields enclosed by brackets "{}"
185
+      replaced with your own identifying information. (Don't include
186
+      the brackets!)  The text should be enclosed in the appropriate
187
+      comment syntax for the file format. We also recommend that a
188
+      file or class name and description of purpose be included on the
189
+      same "printed page" as the copyright notice for easier
190
+      identification within third-party archives.
191
+
192
+   Copyright {yyyy} {name of copyright owner}
193
+
194
+   Licensed under the Apache License, Version 2.0 (the "License");
195
+   you may not use this file except in compliance with the License.
196
+   You may obtain a copy of the License at
197
+
198
+       http://www.apache.org/licenses/LICENSE-2.0
199
+
200
+   Unless required by applicable law or agreed to in writing, software
201
+   distributed under the License is distributed on an "AS IS" BASIS,
202
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
203
+   See the License for the specific language governing permissions and
204
+   limitations under the License.
Back to file index

Makefile

 1
--- 
 2
+++ Makefile
 3
@@ -0,0 +1,8 @@
 4
+#!/usr/bin/make
 5
+PYTHON := /usr/bin/env python
 6
+
 7
+all: lint
 8
+
 9
+lint:
10
+	@flake8 --exclude hooks/charmhelpers hooks tests
11
+	@charm proof
Back to file index

README.md

 1
--- 
 2
+++ README.md
 3
@@ -0,0 +1,43 @@
 4
+# Overview
 5
+
 6
+This charm provides the GitLab open source git repository management platform. 
 7
+
 8
+GitLab includes git repository management, code reviews, issue tracking, wikis and much more. 
 9
+GitLab comes with GitLab CI, an easy to use continuous integration and deployment tool.
10
+
11
+Discuss issues and plan milestones. Do code reviews and make line comments. Mention your colleagues anywhere. View activity streams of projects or people.
12
+
13
+GitLab has integrations for tons of tools such as Slack, Hipchat, LDAP, JIRA, Jenkins, many types of hooks and a complete API.
14
+
15
+# Usage
16
+
17
+To deploy Gitlab simply run:
18
+
19
+    juju deploy cs:~spiculecharms/gitlab-server
20
+    juju expose gitlab-server
21
+
22
+Login to the new server with the username: root and a password of your choice and on first login you will be instructed 
23
+to change the root password.
24
+
25
+To use it with an HTTP endpoint like HA Proxy you need to run
26
+
27
+    juju deploy haproxy
28
+    juju add-relation haproxy gitlab
29
+
30
+This charm also supports the SSL Termination Proxy charm to provide SSL encryption.
31
+
32
+Once deployed you should change the external_url from localhost to the FQDN of your Gitlab client. Whilst it doesn't affect the operation of the server, it does ensure links and urls generated by Gitlab are correct for remote operation.
33
+
34
+    juju config gitlab-server external_url=<your fqdn>
35
+ 
36
+# Limitations
37
+
38
+Curently there is no HA mode or scale out.
39
+
40
+# Code
41
+
42
+Source Code: https://github.com/osbi/layer-gitlab
43
+
44
+# Contact Information
45
+
46
+- <tom@spicule.co.uk>
Back to file index

bin/layer_option

 1
--- 
 2
+++ bin/layer_option
 3
@@ -0,0 +1,24 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+import sys
 7
+sys.path.append('lib')
 8
+
 9
+import argparse
10
+from charms.layer import options
11
+
12
+
13
+parser = argparse.ArgumentParser(description='Access layer options.')
14
+parser.add_argument('section',
15
+                    help='the section, or layer, the option is from')
16
+parser.add_argument('option',
17
+                    help='the option to access')
18
+
19
+args = parser.parse_args()
20
+value = options(args.section).get(args.option, '')
21
+if isinstance(value, bool):
22
+    sys.exit(0 if value else 1)
23
+elif isinstance(value, list):
24
+    for val in value:
25
+        print(val)
26
+else:
27
+    print(value)
Back to file index

config.yaml

  1
--- 
  2
+++ config.yaml
  3
@@ -0,0 +1,121 @@
  4
+"options":
  5
+  "gitlab_version":
  6
+    "type": "string"
  7
+    "description": "The version of GitLab to install"
  8
+    "default": "9.1.0-ce.0"
  9
+  "external_url":
 10
+    "type": "string"
 11
+    "description": "The FQDN for your GitLab unit"
 12
+    "default": "http://localhost"
 13
+  "http_port":
 14
+    "type": "string"
 15
+    "description": "The HTTP port"
 16
+    "default": "80"
 17
+  "ssh_host":
 18
+    "type": "string"
 19
+    "description": "Override the SSH Hostname"
 20
+    "default": !!null ""
 21
+  "time_zone":
 22
+    "type": "string"
 23
+    "description": "Override the default timezone(UTC)"
 24
+    "default": !!null ""
 25
+  "email_from":
 26
+    "type": "string"
 27
+    "description": "Email address used in the from field in mails send by GitLab"
 28
+    "default": !!null ""
 29
+  "from_email_name":
 30
+    "type": "string"
 31
+    "description": "The Name Used in the From field"
 32
+    "default": !!null ""
 33
+  "reply_to_email":
 34
+    "type": "string"
 35
+    "description": "Override the reply to email address"
 36
+    "default": !!null ""
 37
+  "smtp_enable":
 38
+    "type": "boolean"
 39
+    "description": "Enable SMTP sending"
 40
+    "default": !!bool "true"
 41
+  "smtp_address":
 42
+    "type": "string"
 43
+    "description": "SMTP Address"
 44
+    "default": !!null ""
 45
+  "smtp_port":
 46
+    "type": "string"
 47
+    "description": "SMTP Port"
 48
+    "default": !!null ""
 49
+  "smtp_user_name":
 50
+    "type": "string"
 51
+    "description": "SMTP Username"
 52
+    "default": !!null ""
 53
+  "smtp_password":
 54
+    "type": "string"
 55
+    "description": "SMTP Password"
 56
+    "default": !!null ""
 57
+  "smtp_domain":
 58
+    "type": "string"
 59
+    "description": "SMTP Domain"
 60
+    "default": !!null ""
 61
+  "smtp_enable_starttls_auto":
 62
+    "type": "boolean"
 63
+    "description": "Enable STARTTLS automatically"
 64
+    "default": !!bool "true"
 65
+  "smtp_tls":
 66
+    "type": "boolean"
 67
+    "description": "SMTP TLS"
 68
+    "default": !!bool "false"
 69
+  "incoming_email_enabled":
 70
+    "type": "boolean"
 71
+    "description": "Accept incoming email"
 72
+    "default": !!bool "false"
 73
+  "incoming_email_address":
 74
+    "type": "string"
 75
+    "description": "The email address including the `%{key}` placeholder that will\
 76
+      \ be replaced to reference the item being replied to. The `+%{key}` placeholder\
 77
+      \ is added after the user part, after a `+` character, before the `@`."
 78
+    "default": !!null ""
 79
+  "incoming_email_email":
 80
+    "type": "string"
 81
+    "description": "Incoming email address"
 82
+    "default": !!null ""
 83
+  "incoming_email_password":
 84
+    "type": "string"
 85
+    "description": "Incoming email password"
 86
+    "default": !!null ""
 87
+  "incoming_email_host":
 88
+    "type": "string"
 89
+    "description": "Incoming Email Host"
 90
+    "default": !!null ""
 91
+  "incoming_email_port":
 92
+    "type": "string"
 93
+    "description": "Incoming Email Port"
 94
+    "default": !!null ""
 95
+  "incoming_email_ssl":
 96
+    "type": "boolean"
 97
+    "description": "Incoming Email SSL"
 98
+    "default": !!bool "true"
 99
+  "incoming_email_start_tls":
100
+    "type": "boolean"
101
+    "description": "Incoming Email Start TLS"
102
+    "default": !!bool "false"
103
+  "incoming_email_mailbox_name":
104
+    "type": "string"
105
+    "description": "Incoming mailbox name"
106
+    "default": !!null ""
107
+  "backup_path":
108
+    "type": "string"
109
+    "description": "Override the default backup path"
110
+    "default": "/var/opt/gitlab/backups"
111
+  "backup_keep_time":
112
+    "type": "string"
113
+    "description": "Set the default backup keep time"
114
+    "default": "604800"
115
+  "backup_upload_connection":
116
+    "type": "string"
117
+    "description": |
118
+      'Set the upload provider eg: { 'provider' => 'AWS','region' => 'eu-west-1', 'aws_access_key_id' =>
119
+      'AKIAKIAKI', 'aws_secret_access_key' => 'secret123' }'
120
+    "default": !!null ""
121
+  "backup_upload_remote_directory":
122
+    "type": "string"
123
+    "description": "Bucket to upload to"
124
+    "default": !!null ""
Back to file index

copyright

 1
--- 
 2
+++ copyright
 3
@@ -0,0 +1,16 @@
 4
+Format: http://dep.debian.net/deps/dep5/
 5
+
 6
+Files: *
 7
+Copyright: Copyright 2016, OSBI Ltd., All Rights Reserved.
 8
+License: Apache License 2.0
 9
+ Licensed under the Apache License, Version 2.0 (the "License");
10
+ you may not use this file except in compliance with the License.
11
+ You may obtain a copy of the License at
12
+ .
13
+     http://www.apache.org/licenses/LICENSE-2.0
14
+ .
15
+ Unless required by applicable law or agreed to in writing, software
16
+ distributed under the License is distributed on an "AS IS" BASIS,
17
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ See the License for the specific language governing permissions and
19
+ limitations under the License.
Back to file index

data/apache_vhost.conf.j2

 1
--- 
 2
+++ data/apache_vhost.conf.j2
 3
@@ -0,0 +1,59 @@
 4
+# This configuration has been tested on GitLab 8.2
 5
+# Note this config assumes unicorn is listening on default port 8080 and
 6
+# gitlab-workhorse is listening on port 8181. To allow gitlab-workhorse to
 7
+# listen on port 8181, edit /etc/gitlab/gitlab.rb and change the following:
 8
+#
 9
+# gitlab_workhorse['listen_network'] = "tcp"
10
+# gitlab_workhorse['listen_addr'] = "127.0.0.1:8181"
11
+#
12
+#Module dependencies
13
+# mod_rewrite
14
+# mod_proxy
15
+# mod_proxy_http
16
+<VirtualHost *:80>
17
+ServerName YOUR_SERVER_FQDN
18
+ServerSignature Off
19
+
20
+ProxyPreserveHost On
21
+
22
+# Ensure that encoded slashes are not decoded but left in their encoded state.
23
+# http://doc.gitlab.com/ce/api/projects.html#get-single-project
24
+AllowEncodedSlashes NoDecode
25
+
26
+<Location />
27
+# New authorization commands for apache 2.4 and up
28
+# http://httpd.apache.org/docs/2.4/upgrading.html#access
29
+Require all granted
30
+
31
+#Allow forwarding to gitlab-workhorse
32
+ProxyPassReverse http://127.0.0.1:8181
33
+  ProxyPassReverse http://YOUR_SERVER_FQDN/
34
+</Location>
35
+
36
+# Apache equivalent of nginx try files
37
+# http://serverfault.com/questions/290784/what-is-apaches-equivalent-of-nginxs-try-files
38
+# http://stackoverflow.com/questions/10954516/apache2-proxypass-for-rails-app-gitlab
39
+RewriteEngine on
40
+
41
+#Forward these requests to gitlab-workhorse
42
+RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA]
43
+
44
+# needed for downloading attachments
45
+  DocumentRoot /opt/gitlab/embedded/service/gitlab-rails/public
46
+
47
+#Set up apache error documents, if back end goes down (i.e. 503 error) then a maintenance/deploy page is thrown up.
48
+ErrorDocument 404 /404.html
49
+ErrorDocument 422 /422.html
50
+ErrorDocument 500 /500.html
51
+ErrorDocument 503 /deploy.html
52
+
53
+# It is assumed that the log directory is in /var/log/httpd.
54
+# For Debian distributions you might want to change this to
55
+# /var/log/apache2.
56
+LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b" common_forwarded
57
+ErrorLog /var/log/httpd/logs/YOUR_SERVER_FQDN_error.log
58
+CustomLog /var/log/httpd/logs/YOUR_SERVER_FQDN_forwarded.log common_forwarded
59
+CustomLog /var/log/httpd/logs/YOUR_SERVER_FQDN_access.log combined env=!dontlog
60
+CustomLog /var/log/httpd/logs/YOUR_SERVER_FQDN.log combined
61
+
62
+</VirtualHost>
Back to file index

data/gitlab_gitlab-ce.list

1
--- 
2
+++ data/gitlab_gitlab-ce.list
3
@@ -0,0 +1,2 @@
4
+deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ trusty main
5
+deb-src https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ trusty main
Back to file index

data/gitlab_gitlab-ce.list.xenial

1
--- 
2
+++ data/gitlab_gitlab-ce.list.xenial
3
@@ -0,0 +1,2 @@
4
+deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ xenial main
5
+deb-src https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ xenial main
Back to file index

data/gitlab_gpg.key

 1
--- 
 2
+++ data/gitlab_gpg.key
 3
@@ -0,0 +1,30 @@
 4
+-----BEGIN PGP PUBLIC KEY BLOCK-----
 5
+Version: GnuPG v1.4.11 (GNU/Linux)
 6
+
 7
+mQINBFUxDA4BEAC0Pwepk/QZK7QOv6loLtUqmPCJtUuOS3Gu410FoOCgh5agWmXe
 8
+J2pCTejLIMWPEG1Q35lrv5PRlcRA+XLIcYd6x7pF4+sDE1lOZVBndUMSHDReq+r+
 9
+lzRB0Rd6S75RshBRDuwHfBfzjmFcyPqqYdiY3YUqk+hHl/w8m5QlxgLDnp2Vjh2B
10
+yzJqDtJh2+TmvY4XD91Q1fvihZkN3RFBgIjjs4xVQ+wptjg8FsPovgA+QED+hkFc
11
+bBveClexICHi6mTFG+1HV1MfcZnIRDlggTCUj/U8TGnU5crs6GVbbxtKfTCAZYlQ
12
+k5Q2JoPE4156wNFPQ7/Eyr3GnP62oySmuaCDzVVOlnmu4GMTVq/LVQZV3wOAdHM1
13
++9i0ob/SLYT5QKuL5jYj99rz2wy4HWxGR6TrSc/Ls0sc2MvZBeIXpOsPI2rxOeS+
14
+3Kbz8E+0ezNWxHC2LBQezW1ikNfLow/vwIBDCS9ApDAdW8VN28cROoiCMd6yxnVI
15
+1P2nMCkDMCBNqvcWtGrhUvpFD4jfaQ8661GEspqMbrXuNQ//JsrD9n98dJDWdCUV
16
+0LWBEyAJTOV9kIEH128MlPK8SLNkvCBZNJS4pzUxJFmf3LbDmYMuqcgz1d5NltMk
17
+tzVEpVJ4tgZ0gyn4f/yuZHobq6hP1YHgu3lNt7Aibi6dX5pfw2oWqufuPwARAQAB
18
+tEJHaXRMYWIgQi5WLiAocGFja2FnZSByZXBvc2l0b3J5IHNpZ25pbmcga2V5KSA8
19
+cGFja2FnZXNAZ2l0bGFiLmNvbT6JAj4EEwECACgFAlUxDA4CGwMFCQlmAYAGCwkI
20
+BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEBQhmpbhXnj0iN0QAIGHf0CShvrEZXOq
21
+8Tlq+zJ42CQTOLa9Hijd85mqwijgoBwCdLaePaOqOBIkqev3UDfcoMJP9/JuXMpI
22
+9H+JvfY/USwP7FVTpdyC+iecWOSJ/qdbxJEau2wyGwsVhcas9iOExzd6tjsS61Td
23
+1bpdTBYG7eAenCu5WYU/cb0OhPbzRuUiLrtpt43tx2cXIU+XcEC/R9aym7EPw3WG
24
+SePegNhKbtr3LaTuRswgO464LHgJ0YsUx9789QSyuhHtQGznBpBDj0F/xVjnxRs4
25
+6vpd46AWad0G7RhDCWduuG0qx1/1ZBbQKKjRq/1Uw54qiVJB0T/7qtQ9OliUonDj
26
+Vgkj3w1HGXTwKVSkDwEqyn+SDWERA9k04DQrOLEG0qi9NGLYy59v4SaU3ftZw0L6
27
+jnCJksnACtrsksJWPI0Gbs+wbII6fhu8Zc1iV3hdzi92lDMv0W1KzM7FCrz3ex6i
28
+3oL+ntZW/PuHNSUVBlr2FkkSr/EmRkBoD9efZsG7+5vYImtkSZSaiMi5IsexjTEH
29
+HkP0xG0OUaCagSNrNolDyLEmTjhOmky67oE1VIOIbMajXzeNdqYahz8+kBQ5vgpr
30
+0PqlNbnVgCiTlFjTVGHUj84SKh/Gii+GRHlCV1d5UL/GzJppZ5MfpjRXOTamqU/C
31
+O0JLVZiTnW+KSqbLEdflanh8IPTF
32
+=jmzU
33
+-----END PGP PUBLIC KEY BLOCK-----
Back to file index

hooks/config-changed

 1
--- 
 2
+++ hooks/config-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/hook.template

 1
--- 
 2
+++ hooks/hook.template
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/install

 1
--- 
 2
+++ hooks/install
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/leader-elected

 1
--- 
 2
+++ hooks/leader-elected
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/leader-settings-changed

 1
--- 
 2
+++ hooks/leader-settings-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/relations/http/README.md

 1
--- 
 2
+++ hooks/relations/http/README.md
 3
@@ -0,0 +1,68 @@
 4
+# Overview
 5
+
 6
+This interface layer implements the basic form of the `http` interface protocol,
 7
+which is used for things such as reverse-proxies, load-balanced servers, REST
 8
+service discovery, et cetera.
 9
+
10
+# Usage
11
+
12
+## Provides
13
+
14
+By providing the `http` interface, your charm is providing an HTTP server that
15
+can be load-balanced, reverse-proxied, used as a REST endpoint, etc.
16
+
17
+Your charm need only provide the port on which it is serving its content, as
18
+soon as the `{relation_name}.available` state is set:
19
+
20
+```python
21
+@when('website.available')
22
+def configure_website(website):
23
+    website.configure(port=hookenv.config('port'))
24
+```
25
+
26
+## Requires
27
+
28
+By requiring the `http` interface, your charm is consuming one or more HTTP
29
+servers, as a REST endpoint, to load-balance a set of servers, etc.
30
+
31
+Your charm should respond to the `{relation_name}.available` state, which
32
+indicates that there is at least one HTTP server connected.
33
+
34
+The `services()` method returns a list of available HTTP services and their
35
+associated hosts and ports.
36
+
37
+The return value is a list of dicts of the following form:
38
+
39
+```python
40
+[
41
+    {
42
+        'service_name': name_of_service,
43
+        'hosts': [
44
+            {
45
+                'hostname': address_of_host,
46
+                'port': port_for_host,
47
+            },
48
+            # ...
49
+        ],
50
+    },
51
+    # ...
52
+]
53
+```
54
+
55
+A trivial example of handling this interface would be:
56
+
57
+```python
58
+from charms.reactive.helpers import data_changed
59
+
60
+@when('reverseproxy.available')
61
+def update_reverse_proxy_config(reverseproxy):
62
+    services = reverseproxy.services()
63
+    if not data_changed('reverseproxy.services', services):
64
+        return
65
+    for service in services:
66
+        for host in service['hosts']:
67
+            hookenv.log('{} has a unit {}:{}'.format(
68
+                services['service_name'],
69
+                host['hostname'],
70
+                host['port']))
71
+```
Back to file index

hooks/relations/http/interface.yaml

1
--- 
2
+++ hooks/relations/http/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: http
5
+summary: Basic HTTP interface
6
+version: 1
7
+repo: https://git.launchpad.net/~bcsaller/charms/+source/http
Back to file index

hooks/relations/http/provides.py

 1
--- 
 2
+++ hooks/relations/http/provides.py
 3
@@ -0,0 +1,28 @@
 4
+from charmhelpers.core import hookenv
 5
+from charms.reactive import hook
 6
+from charms.reactive import RelationBase
 7
+from charms.reactive import scopes
 8
+
 9
+
10
+class HttpProvides(RelationBase):
11
+    scope = scopes.GLOBAL
12
+
13
+    @hook('{provides:http}-relation-{joined,changed}')
14
+    def changed(self):
15
+        self.set_state('{relation_name}.available')
16
+
17
+    @hook('{provides:http}-relation-{broken,departed}')
18
+    def broken(self):
19
+        self.remove_state('{relation_name}.available')
20
+
21
+    def configure(self, port, private_address=None, hostname=None):
22
+        if not hostname:
23
+            hostname = hookenv.unit_get('private-address')
24
+        if not private_address:
25
+            private_address = hookenv.unit_get('private-address')
26
+        relation_info = {
27
+            'hostname': hostname,
28
+            'private-address': private_address,
29
+            'port': port,
30
+        }
31
+        self.set_remote(**relation_info)
Back to file index

hooks/relations/http/requires.py

 1
--- 
 2
+++ hooks/relations/http/requires.py
 3
@@ -0,0 +1,58 @@
 4
+from charms.reactive import hook
 5
+from charms.reactive import RelationBase
 6
+from charms.reactive import scopes
 7
+
 8
+
 9
+class HttpRequires(RelationBase):
10
+    scope = scopes.UNIT
11
+
12
+    @hook('{requires:http}-relation-{joined,changed}')
13
+    def changed(self):
14
+        conv = self.conversation()
15
+        if conv.get_remote('port'):
16
+            # this unit's conversation has a port, so
17
+            # it is part of the set of available units
18
+            conv.set_state('{relation_name}.available')
19
+
20
+    @hook('{requires:http}-relation-{departed,broken}')
21
+    def broken(self):
22
+        conv = self.conversation()
23
+        conv.remove_state('{relation_name}.available')
24
+
25
+    def services(self):
26
+        """
27
+        Returns a list of available HTTP services and their associated hosts
28
+        and ports.
29
+
30
+        The return value is a list of dicts of the following form::
31
+
32
+            [
33
+                {
34
+                    'service_name': name_of_service,
35
+                    'hosts': [
36
+                        {
37
+                            'hostname': address_of_host,
38
+                            'port': port_for_host,
39
+                        },
40
+                        # ...
41
+                    ],
42
+                },
43
+                # ...
44
+            ]
45
+        """
46
+        services = {}
47
+        for conv in self.conversations():
48
+            service_name = conv.scope.split('/')[0]
49
+            service = services.setdefault(service_name, {
50
+                'service_name': service_name,
51
+                'hosts': [],
52
+            })
53
+            host = conv.get_remote('hostname') or \
54
+                conv.get_remote('private-address')
55
+            port = conv.get_remote('port')
56
+            if host and port:
57
+                service['hosts'].append({
58
+                    'hostname': host,
59
+                    'port': port,
60
+                })
61
+        return [s for s in services.values() if s['hosts']]
Back to file index

hooks/start

 1
--- 
 2
+++ hooks/start
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/stop

 1
--- 
 2
+++ hooks/stop
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/update-status

 1
--- 
 2
+++ hooks/update-status
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/upgrade-charm

 1
--- 
 2
+++ hooks/upgrade-charm
 3
@@ -0,0 +1,28 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import os
 8
+import sys
 9
+sys.path.append('lib')
10
+
11
+# This is an upgrade-charm context, make sure we install latest deps
12
+if not os.path.exists('wheelhouse/.upgrade'):
13
+    open('wheelhouse/.upgrade', 'w').close()
14
+    if os.path.exists('wheelhouse/.bootstrapped'):
15
+        os.unlink('wheelhouse/.bootstrapped')
16
+else:
17
+    os.unlink('wheelhouse/.upgrade')
18
+
19
+from charms.layer import basic
20
+basic.bootstrap_charm_deps()
21
+basic.init_config_states()
22
+
23
+
24
+# This will load and run the appropriate @hook and other decorated
25
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
26
+# and $JUJU_CHARM_DIR/hooks/relations.
27
+#
28
+# See https://jujucharms.com/docs/stable/authors-charm-building
29
+# for more information on this pattern.
30
+from charms.reactive import main
31
+main()
Back to file index

hooks/website-relation-broken

 1
--- 
 2
+++ hooks/website-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/website-relation-changed

 1
--- 
 2
+++ hooks/website-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/website-relation-departed

 1
--- 
 2
+++ hooks/website-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/website-relation-joined

 1
--- 
 2
+++ hooks/website-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

icon.svg

  1
--- 
  2
+++ icon.svg
  3
@@ -0,0 +1,277 @@
  4
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
  5
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
  6
+
  7
+<svg
  8
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
  9
+   xmlns:cc="http://creativecommons.org/ns#"
 10
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 11
+   xmlns:svg="http://www.w3.org/2000/svg"
 12
+   xmlns="http://www.w3.org/2000/svg"
 13
+   xmlns:xlink="http://www.w3.org/1999/xlink"
 14
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 15
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 16
+   width="96"
 17
+   height="96"
 18
+   id="svg6517"
 19
+   version="1.1"
 20
+   inkscape:version="0.91 r13725"
 21
+   sodipodi:docname="icon.svg">
 22
+  <defs
 23
+     id="defs6519">
 24
+    <linearGradient
 25
+       id="Background">
 26
+      <stop
 27
+         id="stop4178"
 28
+         offset="0"
 29
+         style="stop-color:#b8b8b8;stop-opacity:1" />
 30
+      <stop
 31
+         id="stop4180"
 32
+         offset="1"
 33
+         style="stop-color:#c9c9c9;stop-opacity:1" />
 34
+    </linearGradient>
 35
+    <filter
 36
+       style="color-interpolation-filters:sRGB;"
 37
+       inkscape:label="Inner Shadow"
 38
+       id="filter1121">
 39
+      <feFlood
 40
+         flood-opacity="0.59999999999999998"
 41
+         flood-color="rgb(0,0,0)"
 42
+         result="flood"
 43
+         id="feFlood1123" />
 44
+      <feComposite
 45
+         in="flood"
 46
+         in2="SourceGraphic"
 47
+         operator="out"
 48
+         result="composite1"
 49
+         id="feComposite1125" />
 50
+      <feGaussianBlur
 51
+         in="composite1"
 52
+         stdDeviation="1"
 53
+         result="blur"
 54
+         id="feGaussianBlur1127" />
 55
+      <feOffset
 56
+         dx="0"
 57
+         dy="2"
 58
+         result="offset"
 59
+         id="feOffset1129" />
 60
+      <feComposite
 61
+         in="offset"
 62
+         in2="SourceGraphic"
 63
+         operator="atop"
 64
+         result="composite2"
 65
+         id="feComposite1131" />
 66
+    </filter>
 67
+    <filter
 68
+       style="color-interpolation-filters:sRGB;"
 69
+       inkscape:label="Drop Shadow"
 70
+       id="filter950">
 71
+      <feFlood
 72
+         flood-opacity="0.25"
 73
+         flood-color="rgb(0,0,0)"
 74
+         result="flood"
 75
+         id="feFlood952" />
 76
+      <feComposite
 77
+         in="flood"
 78
+         in2="SourceGraphic"
 79
+         operator="in"
 80
+         result="composite1"
 81
+         id="feComposite954" />
 82
+      <feGaussianBlur
 83
+         in="composite1"
 84
+         stdDeviation="1"
 85
+         result="blur"
 86
+         id="feGaussianBlur956" />
 87
+      <feOffset
 88
+         dx="0"
 89
+         dy="1"
 90
+         result="offset"
 91
+         id="feOffset958" />
 92
+      <feComposite
 93
+         in="SourceGraphic"
 94
+         in2="offset"
 95
+         operator="over"
 96
+         result="composite2"
 97
+         id="feComposite960" />
 98
+    </filter>
 99
+    <clipPath
100
+       clipPathUnits="userSpaceOnUse"
101
+       id="clipPath873">
102
+      <g
103
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
104
+         id="g875"
105
+         inkscape:label="Layer 1"
106
+         style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
107
+        <path
108
+           style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
109
+           d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"
110
+           id="path877"
111
+           inkscape:connector-curvature="0"
112
+           sodipodi:nodetypes="sssssssss" />
113
+      </g>
114
+    </clipPath>
115
+    <filter
116
+       inkscape:collect="always"
117
+       id="filter891"
118
+       inkscape:label="Badge Shadow">
119
+      <feGaussianBlur
120
+         inkscape:collect="always"
121
+         stdDeviation="0.71999962"
122
+         id="feGaussianBlur893" />
123
+    </filter>
124
+  </defs>
125
+  <sodipodi:namedview
126
+     id="base"
127
+     pagecolor="#ffffff"
128
+     bordercolor="#666666"
129
+     borderopacity="1.0"
130
+     inkscape:pageopacity="0.0"
131
+     inkscape:pageshadow="2"
132
+     inkscape:zoom="4.0745362"
133
+     inkscape:cx="-17.317629"
134
+     inkscape:cy="29.384032"
135
+     inkscape:document-units="px"
136
+     inkscape:current-layer="layer3"
137
+     showgrid="true"
138
+     fit-margin-top="0"
139
+     fit-margin-left="0"
140
+     fit-margin-right="0"
141
+     fit-margin-bottom="0"
142
+     inkscape:window-width="1600"
143
+     inkscape:window-height="876"
144
+     inkscape:window-x="0"
145
+     inkscape:window-y="24"
146
+     inkscape:window-maximized="1"
147
+     showborder="true"
148
+     showguides="true"
149
+     inkscape:guide-bbox="true"
150
+     inkscape:showpageshadow="false">
151
+    <inkscape:grid
152
+       type="xygrid"
153
+       id="grid821" />
154
+    <sodipodi:guide
155
+       orientation="1,0"
156
+       position="16,48"
157
+       id="guide823" />
158
+    <sodipodi:guide
159
+       orientation="0,1"
160
+       position="64,80"
161
+       id="guide825" />
162
+    <sodipodi:guide
163
+       orientation="1,0"
164
+       position="80,40"
165
+       id="guide827" />
166
+    <sodipodi:guide
167
+       orientation="0,1"
168
+       position="64,16"
169
+       id="guide829" />
170
+  </sodipodi:namedview>
171
+  <metadata
172
+     id="metadata6522">
173
+    <rdf:RDF>
174
+      <cc:Work
175
+         rdf:about="">
176
+        <dc:format>image/svg+xml</dc:format>
177
+        <dc:type
178
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
179
+        <dc:title />
180
+      </cc:Work>
181
+    </rdf:RDF>
182
+  </metadata>
183
+  <g
184
+     inkscape:label="BACKGROUND"
185
+     inkscape:groupmode="layer"
186
+     id="layer1"
187
+     transform="translate(268,-635.29076)"
188
+     style="display:inline">
189
+    <path
190
+       style="fill:#333c47;fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
191
+       d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"
192
+       id="path6455"
193
+       inkscape:connector-curvature="0"
194
+       sodipodi:nodetypes="sssssssss" />
195
+  </g>
196
+  <g
197
+     inkscape:groupmode="layer"
198
+     id="layer3"
199
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"
200
+     style="display:inline">
201
+    <image
202
+       y="19.344263"
203
+       x="16"
204
+       id="image3307"
205
+       xlink:href="data:image/png;base64,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 "
206
+       height="56.655739"
207
+       width="64" />
208
+  </g>
209
+  <g
210
+     inkscape:groupmode="layer"
211
+     id="layer2"
212
+     inkscape:label="BADGE"
213
+     style="display:none"
214
+     sodipodi:insensitive="true">
215
+    <g
216
+       style="display:inline"
217
+       transform="translate(-340.00001,-581)"
218
+       id="g4394"
219
+       clip-path="none">
220
+      <g
221
+         id="g855">
222
+        <g
223
+           inkscape:groupmode="maskhelper"
224
+           id="g870"
225
+           clip-path="url(#clipPath873)"
226
+           style="opacity:0.6;filter:url(#filter891)">
227
+          <path
228
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
229
+             d="m 264,552.36218 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z"
230
+             sodipodi:ry="12"
231
+             sodipodi:rx="12"
232
+             sodipodi:cy="552.36218"
233
+             sodipodi:cx="252"
234
+             id="path844"
235
+             style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
236
+             sodipodi:type="arc" />
237
+        </g>
238
+        <g
239
+           id="g862">
240
+          <path
241
+             sodipodi:type="arc"
242
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
243
+             id="path4398"
244
+             sodipodi:cx="252"
245
+             sodipodi:cy="552.36218"
246
+             sodipodi:rx="12"
247
+             sodipodi:ry="12"
248
+             d="m 264,552.36218 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z"
249
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
250
+          <path
251
+             transform="matrix(1.25,0,0,1.25,33,-100.45273)"
252
+             d="m 264,552.36218 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z"
253
+             sodipodi:ry="12"
254
+             sodipodi:rx="12"
255
+             sodipodi:cy="552.36218"
256
+             sodipodi:cx="252"
257
+             id="path4400"
258
+             style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
259
+             sodipodi:type="arc" />
260
+          <path
261
+             sodipodi:type="star"
262
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
263
+             id="path4459"
264
+             sodipodi:sides="5"
265
+             sodipodi:cx="666.19574"
266
+             sodipodi:cy="589.50385"
267
+             sodipodi:r1="7.2431178"
268
+             sodipodi:r2="4.3458705"
269
+             sodipodi:arg1="1.0471976"
270
+             sodipodi:arg2="1.6755161"
271
+             inkscape:flatsided="false"
272
+             inkscape:rounded="0.1"
273
+             inkscape:randomized="0"
274
+             d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 -0.18379,0.41279 0.0427,4.27917 -0.34859,4.5051 z"
275
+             transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
276
+        </g>
277
+      </g>
278
+    </g>
279
+  </g>
280
+</svg>
Back to file index

layer.yaml

 1
--- 
 2
+++ layer.yaml
 3
@@ -0,0 +1,12 @@
 4
+"options":
 5
+  "basic":
 6
+    "packages":
 7
+    - "apt-transport-https"
 8
+    "use_venv": !!bool "false"
 9
+    "include_system_packages": !!bool "false"
10
+  "gitlab-server": {}
11
+"includes":
12
+- "layer:basic"
13
+- "interface:http"
14
+"repo": "git@github.com:OSBI/layer-gitlab.git"
15
+"is": "gitlab-server"
Back to file index

lib/charms/layer/__init__.py

 1
--- 
 2
+++ lib/charms/layer/__init__.py
 3
@@ -0,0 +1,21 @@
 4
+import os
 5
+
 6
+
 7
+class LayerOptions(dict):
 8
+    def __init__(self, layer_file, section=None):
 9
+        import yaml  # defer, might not be available until bootstrap
10
+        with open(layer_file) as f:
11
+            layer = yaml.safe_load(f.read())
12
+        opts = layer.get('options', {})
13
+        if section and section in opts:
14
+            super(LayerOptions, self).__init__(opts.get(section))
15
+        else:
16
+            super(LayerOptions, self).__init__(opts)
17
+
18
+
19
+def options(section=None, layer_file=None):
20
+    if not layer_file:
21
+        base_dir = os.environ.get('JUJU_CHARM_DIR', os.getcwd())
22
+        layer_file = os.path.join(base_dir, 'layer.yaml')
23
+
24
+    return LayerOptions(layer_file, section)
Back to file index

lib/charms/layer/basic.py

  1
--- 
  2
+++ lib/charms/layer/basic.py
  3
@@ -0,0 +1,205 @@
  4
+import os
  5
+import sys
  6
+import shutil
  7
+from glob import glob
  8
+from subprocess import check_call, CalledProcessError
  9
+from time import sleep
 10
+
 11
+from charms.layer.execd import execd_preinstall
 12
+
 13
+
 14
+def lsb_release():
 15
+    """Return /etc/lsb-release in a dict"""
 16
+    d = {}
 17
+    with open('/etc/lsb-release', 'r') as lsb:
 18
+        for l in lsb:
 19
+            k, v = l.split('=')
 20
+            d[k.strip()] = v.strip()
 21
+    return d
 22
+
 23
+
 24
+def bootstrap_charm_deps():
 25
+    """
 26
+    Set up the base charm dependencies so that the reactive system can run.
 27
+    """
 28
+    # execd must happen first, before any attempt to install packages or
 29
+    # access the network, because sites use this hook to do bespoke
 30
+    # configuration and install secrets so the rest of this bootstrap
 31
+    # and the charm itself can actually succeed. This call does nothing
 32
+    # unless the operator has created and populated $JUJU_CHARM_DIR/exec.d.
 33
+    execd_preinstall()
 34
+    # ensure that $JUJU_CHARM_DIR/bin is on the path, for helper scripts
 35
+    charm_dir = os.environ['JUJU_CHARM_DIR']
 36
+    os.environ['PATH'] += ':%s' % os.path.join(charm_dir, 'bin')
 37
+    venv = os.path.abspath('../.venv')
 38
+    vbin = os.path.join(venv, 'bin')
 39
+    vpip = os.path.join(vbin, 'pip')
 40
+    vpy = os.path.join(vbin, 'python')
 41
+    if os.path.exists('wheelhouse/.bootstrapped'):
 42
+        activate_venv()
 43
+        return
 44
+    # bootstrap wheelhouse
 45
+    if os.path.exists('wheelhouse'):
 46
+        with open('/root/.pydistutils.cfg', 'w') as fp:
 47
+            # make sure that easy_install also only uses the wheelhouse
 48
+            # (see https://github.com/pypa/pip/issues/410)
 49
+            fp.writelines([
 50
+                "[easy_install]\n",
 51
+                "allow_hosts = ''\n",
 52
+                "find_links = file://{}/wheelhouse/\n".format(charm_dir),
 53
+            ])
 54
+        apt_install([
 55
+            'python3-pip',
 56
+            'python3-setuptools',
 57
+            'python3-yaml',
 58
+            'python3-dev',
 59
+        ])
 60
+        from charms import layer
 61
+        cfg = layer.options('basic')
 62
+        # include packages defined in layer.yaml
 63
+        apt_install(cfg.get('packages', []))
 64
+        # if we're using a venv, set it up
 65
+        if cfg.get('use_venv'):
 66
+            if not os.path.exists(venv):
 67
+                series = lsb_release()['DISTRIB_CODENAME']
 68
+                if series in ('precise', 'trusty'):
 69
+                    apt_install(['python-virtualenv'])
 70
+                else:
 71
+                    apt_install(['virtualenv'])
 72
+                cmd = ['virtualenv', '-ppython3', '--never-download', venv]
 73
+                if cfg.get('include_system_packages'):
 74
+                    cmd.append('--system-site-packages')
 75
+                check_call(cmd)
 76
+            os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
 77
+            pip = vpip
 78
+        else:
 79
+            pip = 'pip3'
 80
+            # save a copy of system pip to prevent `pip3 install -U pip`
 81
+            # from changing it
 82
+            if os.path.exists('/usr/bin/pip'):
 83
+                shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save')
 84
+        # need newer pip, to fix spurious Double Requirement error:
 85
+        # https://github.com/pypa/pip/issues/56
 86
+        check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse',
 87
+                    'pip'])
 88
+        # install the rest of the wheelhouse deps
 89
+        check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
 90
+                   glob('wheelhouse/*'))
 91
+        if not cfg.get('use_venv'):
 92
+            # restore system pip to prevent `pip3 install -U pip`
 93
+            # from changing it
 94
+            if os.path.exists('/usr/bin/pip.save'):
 95
+                shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip')
 96
+                os.remove('/usr/bin/pip.save')
 97
+        os.remove('/root/.pydistutils.cfg')
 98
+        # flag us as having already bootstrapped so we don't do it again
 99
+        open('wheelhouse/.bootstrapped', 'w').close()
100
+        # Ensure that the newly bootstrapped libs are available.
101
+        # Note: this only seems to be an issue with namespace packages.
102
+        # Non-namespace-package libs (e.g., charmhelpers) are available
103
+        # without having to reload the interpreter. :/
104
+        reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0])
105
+
106
+
107
+def activate_venv():
108
+    """
109
+    Activate the venv if enabled in ``layer.yaml``.
110
+
111
+    This is handled automatically for normal hooks, but actions might
112
+    need to invoke this manually, using something like:
113
+
114
+        # Load modules from $JUJU_CHARM_DIR/lib
115
+        import sys
116
+        sys.path.append('lib')
117
+
118
+        from charms.layer.basic import activate_venv
119
+        activate_venv()
120
+
121
+    This will ensure that modules installed in the charm's
122
+    virtual environment are available to the action.
123
+    """
124
+    venv = os.path.abspath('../.venv')
125
+    vbin = os.path.join(venv, 'bin')
126
+    vpy = os.path.join(vbin, 'python')
127
+    from charms import layer
128
+    cfg = layer.options('basic')
129
+    if cfg.get('use_venv') and '.venv' not in sys.executable:
130
+        # activate the venv
131
+        os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
132
+        reload_interpreter(vpy)
133
+
134
+
135
+def reload_interpreter(python):
136
+    """
137
+    Reload the python interpreter to ensure that all deps are available.
138
+
139
+    Newly installed modules in namespace packages sometimes seemt to
140
+    not be picked up by Python 3.
141
+    """
142
+    os.execve(python, [python] + list(sys.argv), os.environ)
143
+
144
+
145
+def apt_install(packages):
146
+    """
147
+    Install apt packages.
148
+
149
+    This ensures a consistent set of options that are often missed but
150
+    should really be set.
151
+    """
152
+    if isinstance(packages, (str, bytes)):
153
+        packages = [packages]
154
+
155
+    env = os.environ.copy()
156
+
157
+    if 'DEBIAN_FRONTEND' not in env:
158
+        env['DEBIAN_FRONTEND'] = 'noninteractive'
159
+
160
+    cmd = ['apt-get',
161
+           '--option=Dpkg::Options::=--force-confold',
162
+           '--assume-yes',
163
+           'install']
164
+    for attempt in range(3):
165
+        try:
166
+            check_call(cmd + packages, env=env)
167
+        except CalledProcessError:
168
+            if attempt == 2:  # third attempt
169
+                raise
170
+            sleep(5)
171
+        else:
172
+            break
173
+
174
+
175
+def init_config_states():
176
+    import yaml
177
+    from charmhelpers.core import hookenv
178
+    from charms.reactive import set_state
179
+    from charms.reactive import toggle_state
180
+    config = hookenv.config()
181
+    config_defaults = {}
182
+    config_defs = {}
183
+    config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
184
+    if os.path.exists(config_yaml):
185
+        with open(config_yaml) as fp:
186
+            config_defs = yaml.safe_load(fp).get('options', {})
187
+            config_defaults = {key: value.get('default')
188
+                               for key, value in config_defs.items()}
189
+    for opt in config_defs.keys():
190
+        if config.changed(opt):
191
+            set_state('config.changed')
192
+            set_state('config.changed.{}'.format(opt))
193
+        toggle_state('config.set.{}'.format(opt), config.get(opt))
194
+        toggle_state('config.default.{}'.format(opt),
195
+                     config.get(opt) == config_defaults[opt])
196
+    hookenv.atexit(clear_config_states)
197
+
198
+
199
+def clear_config_states():
200
+    from charmhelpers.core import hookenv, unitdata
201
+    from charms.reactive import remove_state
202
+    config = hookenv.config()
203
+    remove_state('config.changed')
204
+    for opt in config.keys():
205
+        remove_state('config.changed.{}'.format(opt))
206
+        remove_state('config.set.{}'.format(opt))
207
+        remove_state('config.default.{}'.format(opt))
208
+    unitdata.kv().flush()
Back to file index

lib/charms/layer/execd.py

  1
--- 
  2
+++ lib/charms/layer/execd.py
  3
@@ -0,0 +1,138 @@
  4
+# Copyright 2014-2016 Canonical Limited.
  5
+#
  6
+# This file is part of layer-basic, the reactive base layer for Juju.
  7
+#
  8
+# charm-helpers is free software: you can redistribute it and/or modify
  9
+# it under the terms of the GNU Lesser General Public License version 3 as
 10
+# published by the Free Software Foundation.
 11
+#
 12
+# charm-helpers is distributed in the hope that it will be useful,
 13
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
 14
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15
+# GNU Lesser General Public License for more details.
 16
+#
 17
+# You should have received a copy of the GNU Lesser General Public License
 18
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 19
+
 20
+# This module may only import from the Python standard library.
 21
+import os
 22
+import sys
 23
+import subprocess
 24
+import time
 25
+
 26
+'''
 27
+execd/preinstall
 28
+
 29
+It is often necessary to configure and reconfigure machines
 30
+after provisioning, but before attempting to run the charm.
 31
+Common examples are specialized network configuration, enabling
 32
+of custom hardware, non-standard disk partitioning and filesystems,
 33
+adding secrets and keys required for using a secured network.
 34
+
 35
+The reactive framework's base layer invokes this mechanism as
 36
+early as possible, before any network access is made or dependencies
 37
+unpacked or non-standard modules imported (including the charms.reactive
 38
+framework itself).
 39
+
 40
+Operators needing to use this functionality may branch a charm and
 41
+create an exec.d directory in it. The exec.d directory in turn contains
 42
+one or more subdirectories, each of which contains an executable called
 43
+charm-pre-install and any other required resources. The charm-pre-install
 44
+executables are run, and if successful, state saved so they will not be
 45
+run again.
 46
+
 47
+    $JUJU_CHARM_DIR/exec.d/mynamespace/charm-pre-install
 48
+
 49
+An alternative to branching a charm is to compose a new charm that contains
 50
+the exec.d directory, using the original charm as a layer,
 51
+
 52
+A charm author could also abuse this mechanism to modify the charm
 53
+environment in unusual ways, but for most purposes it is saner to use
 54
+charmhelpers.core.hookenv.atstart().
 55
+'''
 56
+
 57
+
 58
+def default_execd_dir():
 59
+    return os.path.join(os.environ['JUJU_CHARM_DIR'], 'exec.d')
 60
+
 61
+
 62
+def execd_module_paths(execd_dir=None):
 63
+    """Generate a list of full paths to modules within execd_dir."""
 64
+    if not execd_dir:
 65
+        execd_dir = default_execd_dir()
 66
+
 67
+    if not os.path.exists(execd_dir):
 68
+        return
 69
+
 70
+    for subpath in os.listdir(execd_dir):
 71
+        module = os.path.join(execd_dir, subpath)
 72
+        if os.path.isdir(module):
 73
+            yield module
 74
+
 75
+
 76
+def execd_submodule_paths(command, execd_dir=None):
 77
+    """Generate a list of full paths to the specified command within exec_dir.
 78
+    """
 79
+    for module_path in execd_module_paths(execd_dir):
 80
+        path = os.path.join(module_path, command)
 81
+        if os.access(path, os.X_OK) and os.path.isfile(path):
 82
+            yield path
 83
+
 84
+
 85
+def execd_sentinel_path(submodule_path):
 86
+    module_path = os.path.dirname(submodule_path)
 87
+    execd_path = os.path.dirname(module_path)
 88
+    module_name = os.path.basename(module_path)
 89
+    submodule_name = os.path.basename(submodule_path)
 90
+    return os.path.join(execd_path,
 91
+                        '.{}_{}.done'.format(module_name, submodule_name))
 92
+
 93
+
 94
+def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
 95
+    """Run command for each module within execd_dir which defines it."""
 96
+    if stderr is None:
 97
+        stderr = sys.stdout
 98
+    for submodule_path in execd_submodule_paths(command, execd_dir):
 99
+        # Only run each execd once. We cannot simply run them in the
100
+        # install hook, as potentially storage hooks are run before that.
101
+        # We cannot rely on them being idempotent.
102
+        sentinel = execd_sentinel_path(submodule_path)
103
+        if os.path.exists(sentinel):
104
+            continue
105
+
106
+        try:
107
+            subprocess.check_call([submodule_path], stderr=stderr,
108
+                                  universal_newlines=True)
109
+            with open(sentinel, 'w') as f:
110
+                f.write('{} ran successfully {}\n'.format(submodule_path,
111
+                                                          time.ctime()))
112
+                f.write('Removing this file will cause it to be run again\n')
113
+        except subprocess.CalledProcessError as e:
114
+            # Logs get the details. We can't use juju-log, as the
115
+            # output may be substantial and exceed command line
116
+            # length limits.
117
+            print("ERROR ({}) running {}".format(e.returncode, e.cmd),
118
+                  file=stderr)
119
+            print("STDOUT<<EOM", file=stderr)
120
+            print(e.output, file=stderr)
121
+            print("EOM", file=stderr)
122
+
123
+            # Unit workload status gets a shorter fail message.
124
+            short_path = os.path.relpath(submodule_path)
125
+            block_msg = "Error ({}) running {}".format(e.returncode,
126
+                                                       short_path)
127
+            try:
128
+                subprocess.check_call(['status-set', 'blocked', block_msg],
129
+                                      universal_newlines=True)
130
+                if stop_on_error:
131
+                    sys.exit(0)  # Leave unit in blocked state.
132
+            except Exception:
133
+                pass  # We care about the exec.d/* failure, not status-set.
134
+
135
+            if stop_on_error:
136
+                sys.exit(e.returncode or 1)  # Error state for pre-1.24 Juju
137
+
138
+
139
+def execd_preinstall(execd_dir=None):
140
+    """Run charm-pre-install for each module within execd_dir."""
141
+    execd_run('charm-pre-install', execd_dir=execd_dir)
Back to file index

metadata.yaml

 1
--- 
 2
+++ metadata.yaml
 3
@@ -0,0 +1,17 @@
 4
+"name": "gitlab-server"
 5
+"summary": "Gitlab CE charm."
 6
+"maintainers":
 7
+- "Tom Barber <tom@spicule.co.uk>"
 8
+"description": |
 9
+  This charm will install and configure Gitlab CE.
10
+  Gitlab is a Git repository management system similar
11
+  to Github.
12
+"tags":
13
+- "version_control"
14
+- "application"
15
+"series":
16
+- "xenial"
17
+- "trusty"
18
+"provides":
19
+  "website":
20
+    "interface": "http"
Back to file index

reactive/gitlab.py

  1
--- 
  2
+++ reactive/gitlab.py
  3
@@ -0,0 +1,214 @@
  4
+import fileinput
  5
+import sys
  6
+from charmhelpers.fetch import apt_install
  7
+from charms.reactive import when, when_not, set_state, remove_state, hook
  8
+from subprocess import check_call, CalledProcessError, call
  9
+from charmhelpers.core import hookenv
 10
+from charmhelpers.core.hookenv import status_set
 11
+from charms.reactive.helpers import data_changed
 12
+import re
 13
+import shutil
 14
+from charmhelpers.core.hookenv import log
 15
+
 16
+filepath = '/etc/gitlab/gitlab.rb'
 17
+
 18
+
 19
+@when('website.available')
 20
+def configure_website(website):
 21
+    log("starting hook")
 22
+    #modConfig(filepath, 'gitlab_workhorse[\'listen_network\']', 'tcp')
 23
+    #modConfig(filepath, 'gitlab_workhorse[\'listen_addr\']', '0.0.0.0')
 24
+    #modConfig(filepath, 'nginx[\'enable\']', 'false')
 25
+    #modConfig(filepath, 'web_server[\'external_users\']', 'www-data')
 26
+    #check_call(['gitlab-ctl', 'reconfigure'])
 27
+    #check_call(['gitlab-ctl', 'restart'])
 28
+    website.configure(port=80)
 29
+    set_state('gitlab.external_webserver')
 30
+
 31
+@when_not('website.available')
 32
+def unconfigure_website():
 33
+    #modConfig(filepath, 'gitlab_workhorse[\'listen_network\']', None)
 34
+    #modConfig(filepath, 'gitlab_workhorse[\'listen_addr\']', None)
 35
+    #modConfig(filepath, 'nginx[\'enable\']', 'true')
 36
+    #modConfig(filepath, 'web_server[\'external_users\']', None)
 37
+    set_state('gitlab.internal_webserver')
 38
+
 39
+
 40
+@when_not('gitlab.installed')
 41
+def install():
 42
+    status_set('maintenance', 'Installing GitLab')
 43
+    apt_install(["curl", "openssh-server", "ca-certificates", "postfix"])
 44
+
 45
+    check_call(['apt-key', 'add', './data/gitlab_gpg.key'])
 46
+    if(lsb_release()['DISTRIB_CODENAME'] == "trusty"):
 47
+       shutil.copy2('data/gitlab_gitlab-ce.list', '/etc/apt/sources.list.d/gitlab-ce.list')
 48
+    elif(lsb_release()['DISTRIB_CODENAME'] == "xenial"):
 49
+       shutil.copy2('data/gitlab_gitlab-ce.list.xenial', '/etc/apt/sources.list.d/gitlab-ce.list')
 50
+
 51
+    check_call(['apt-get', 'update'])
 52
+
 53
+    version = ''
 54
+    if hookenv.config('gitlab_version'):
 55
+        version = '=' + hookenv.config('gitlab_version')
 56
+
 57
+    check_call(['apt-get', 'install', 'gitlab-ce' + version])
 58
+    check_call(['gitlab-ctl', 'reconfigure'])
 59
+    set_state('gitlab.installed')
 60
+    status_set('active', 'GitLab is ready!')
 61
+
 62
+
 63
+@when('gitlab.installed')
 64
+def check_running():
 65
+    if data_changed('gitlab.config', hookenv.config()):
 66
+        status_set('maintenance', 'Updating Config')
 67
+        updateConfig(hookenv.config())
 68
+
 69
+    if hookenv.config('http_port'):
 70
+        hookenv.open_port(hookenv.config('http_port'))
 71
+    else:
 72
+        hookenv.open_port(80)
 73
+
 74
+    status_set('active', 'GitLab is ready!')
 75
+
 76
+
 77
+def updateConfig(config):
 78
+    exturl = None
 79
+
 80
+    if hookenv.config('external_url'):
 81
+        exturl = hookenv.config('external_url')
 82
+        if not exturl.startswith("http"):
 83
+            exturl = "http://" + exturl
 84
+
 85
+    if hookenv.config('external_url') and hookenv.config('http_port'):
 86
+        if exturl.endswith("/"):
 87
+            exturl = exturl[:-1]
 88
+
 89
+        exturl = exturl + ":" + hookenv.config('http_port')
 90
+
 91
+    modConfigNoEquals(filepath, 'external_url', exturl)
 92
+    modConfig(filepath, 'gitlab_rails[\'gitlab_ssh_host\']', hookenv.config('ssh_host'))
 93
+    modConfig(filepath, 'gitlab_rails[\'time_zone\']', hookenv.config('time_zone'))
 94
+    modConfig(filepath, 'gitlab_rails[\'gitlab_email_from\']', hookenv.config('email_from'))
 95
+    modConfig(filepath, 'gitlab_rails[\'gitlab_email_display_name\']', hookenv.config('from_email_name'))
 96
+    modConfig(filepath, 'gitlab_rails[\'gitlab_email_reply_to\']', hookenv.config('reply_to_email'))
 97
+    modConfig(filepath, 'gitlab_rails[\'smtp_enable\']', hookenv.config('smtp_enable'))
 98
+    modConfig(filepath, 'gitlab_rails[\'smtp_address\']', hookenv.config('smtp_address'))
 99
+    modConfig(filepath, 'gitlab_rails[\'smtp_port\']', hookenv.config('smtp_port'))
100
+    modConfig(filepath, 'gitlab_rails[\'smtp_user_name\']', hookenv.config('smtp_user_name'))
101
+    modConfig(filepath, 'gitlab_rails[\'smtp_password\']', hookenv.config('smtp_password'))
102
+    modConfig(filepath, 'gitlab_rails[\'smtp_domain\']', hookenv.config('smtp_domain'))
103
+    modConfig(filepath, 'gitlab_rails[\'smtp_enable_starttls_auto\']', hookenv.config('smtp_enable_starttls_auto'))
104
+    modConfig(filepath, 'gitlab_rails[\'smtp_tls\']', hookenv.config('smtp_tls'))
105
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_enabled\']', hookenv.config('incoming_email_enabled'))
106
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_address\']', hookenv.config('incoming_email_address'))
107
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_email\']', hookenv.config('incoming_email_email'))
108
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_password\']', hookenv.config('incoming_email_password'))
109
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_host\']', hookenv.config('incoming_email_host'))
110
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_port\']', hookenv.config('incoming_email_port'))
111
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_ssl\']', hookenv.config('incoming_email_ssl'))
112
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_start_tls\']', hookenv.config('incoming_email_start_tls'))
113
+    modConfig(filepath, 'gitlab_rails[\'incoming_email_mailbox_name\']', hookenv.config('incoming_email_mailbox_name'))
114
+    modConfig(filepath, 'gitlab_rails[\'backup_path\']', hookenv.config('backup_path'))
115
+    modConfig(filepath, 'gitlab_rails[\'backup_keep_time\']', hookenv.config('backup_keep_time'))
116
+    modConfig(filepath, 'gitlab_rails[\'backup_upload_remote_directory\']',
117
+              hookenv.config('backup_upload_remote_directory'))
118
+    modConfig(filepath, 'gitlab_rails[\'backup_upload_connection\']', hookenv.config('backup_upload_connection'))
119
+
120
+    check_call(["gitlab-ctl", "reconfigure"])
121
+
122
+    status_set('active', 'GitLab is ready!')
123
+
124
+
125
+def isfloat(value):
126
+    try:
127
+        float(value)
128
+        return True
129
+    except ValueError:
130
+        return False
131
+
132
+
133
+def modConfigNoEquals(File, Variable, Setting):
134
+    try:
135
+        for line in fileinput.input(File, inplace=1):
136
+            if line.startswith(Variable):
137
+                line = Variable + ' \'' + Setting + '\''
138
+            sys.stdout.write(line)
139
+        fileinput.close()
140
+    except:
141
+        log("encoding error")
142
+        fileinput.close()
143
+        pass
144
+
145
+
146
+def modConfig(File, Variable, Setting):
147
+    """
148
+    Modify Config file variable with new setting
149
+    """
150
+    VarFound = False
151
+    AlreadySet = False
152
+    V = str(Variable)
153
+    S = str(Setting)
154
+    if isinstance(Setting, bool):
155
+        if (Setting):
156
+            S = "true"
157
+        else:
158
+            S = "false"
159
+    elif (S.isdigit()):
160
+        S = int(S)
161
+    elif (isfloat(S)):
162
+        S = float(S)
163
+    else:
164
+        S = '\'' + S + '\''
165
+
166
+    try:
167
+        for line in fileinput.input(File, inplace=1):
168
+            # process lines that look like config settings #
169
+            if '=' in line:
170
+                _infile_var = str(line.split('=')[0].rstrip(' '))
171
+                _infile_set = str(line.split('=')[1].lstrip(' ').rstrip())
172
+                # only change the first matching occurrence #
173
+                if VarFound == False and _infile_var.rstrip(' ') == V:
174
+                    VarFound = True
175
+                    # don't change it if it is already set #
176
+                    if _infile_set.lstrip(' ') == S:
177
+                        AlreadySet = True
178
+                    else:
179
+                        line = "%s = %s\n" % (V, S)
180
+
181
+            sys.stdout.write(line)
182
+    except:
183
+         log("encoding error")
184
+         fileinput.close()
185
+         pass
186
+    # Append the variable if it wasn't found #
187
+    if not VarFound:
188
+        print("Variable '%s' not found.  Adding it to %s" % (V, File))
189
+        with open(File, "a") as f:
190
+            l = "%s = %s\n" % (V, S)
191
+            if (Setting is '' or Setting is None) and not l.lstrip(' ').startswith('#'):
192
+                l = '#' + l
193
+                f.write(l)
194
+            elif (Setting is not '' or Setting is not None) and l.lstrip(' ').startswith('#'):
195
+                l = re.sub("#", "", l)
196
+                f.write(l)
197
+            elif (Setting is not '' or Setting is not None):
198
+                f.write(l)
199
+
200
+    elif AlreadySet == True:
201
+        print("Variable '%s' unchanged" % (V))
202
+    else:
203
+        print("Variable '%s' modified to '%s'" % (V, S))
204
+
205
+    fileinput.close()
206
+    return
207
+
208
+
209
+def lsb_release():
210
+    """Return /etc/lsb-release in a dict"""
211
+    d = {}
212
+    with open('/etc/lsb-release', 'r') as lsb:
213
+        for l in lsb:
214
+            k, v = l.split('=')
215
+            d[k.strip()] = v.strip()
216
+    return d
217
+
Back to file index

requirements.txt

1
--- 
2
+++ requirements.txt
3
@@ -0,0 +1,2 @@
4
+flake8
5
+pytest
Back to file index

revision

1
--- 
2
+++ revision
3
@@ -0,0 +1 @@
4
+0
Back to file index

tests/01-deploy.py

 1
--- 
 2
+++ tests/01-deploy.py
 3
@@ -0,0 +1,43 @@
 4
+#!/usr/bin/python3
 5
+
 6
+# import os
 7
+import amulet
 8
+import requests
 9
+
10
+d = amulet.Deployment(series='xenial')
11
+d.add('gitlab', 'cs:~spiculecharms/gitlab-server')
12
+d.expose('gitlab')
13
+
14
+d.add('haproxy', 'cs:haproxy')
15
+d.expose('haproxy')
16
+
17
+try:
18
+    d.setup(timeout=1200)
19
+    d.sentry.wait()
20
+except amulet.helpers.TimeoutError:
21
+    amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
22
+except:
23
+    raise
24
+
25
+gitlab_unit = d.sentry['gitlab'][0]
26
+haproxy_unit = d.sentry['haproxy'][0]
27
+
28
+home_page = requests.get('http://%s:80/' % gitlab_unit.info['public-address'])
29
+home_page.raise_for_status()  # Make sure it's not 5XX error
30
+assert "GitLab Community Edition" in home_page.text
31
+
32
+d.relate('haproxy:reverseproxy', 'gitlab:website')
33
+
34
+home_page = requests.get('http://%s:80/' % haproxy_unit.info['public-address'])
35
+home_page.raise_for_status()
36
+
37
+assert "GitLab Community Edition" in home_page.text
38
+
39
+
40
+d.configure('gitlab', {
41
+    'external_url': 'http://test.spiculecharms.com',
42
+})
43
+
44
+contents = d.sentry['gitlab/0'].file_contents('/etc/gitlab/gitlab.rb')
45
+
46
+assert "test.spiculecharms.com" in contents
Back to file index

tests/tests.yaml

1
--- 
2
+++ tests/tests.yaml
3
@@ -0,0 +1,2 @@
4
+packages:
5
+  - amulet
Back to file index

tox.ini

 1
--- 
 2
+++ tox.ini
 3
@@ -0,0 +1,12 @@
 4
+[tox]
 5
+skipsdist=True
 6
+envlist = py34, py35
 7
+skip_missing_interpreters = True
 8
+
 9
+[testenv]
10
+commands = py.test -v
11
+deps =
12
+    -r{toxinidir}/requirements.txt
13
+
14
+[flake8]
15
+exclude=docs