diff --git a/changelog b/changelog index 91a4b0d..ecedccd 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,14 @@ +turnkey-gitlab-18.1 (1) turnkey; urgency=low + + * Update GitLab to latest GitLab-CE v17.3.0- via upstream apt repo. + + * Confconsole: Bugfix, improve and update GitLab specific Let's Encrypt + plugin - closes #1975. + + * Inithooks: Improve and update GitLab inithook + + -- Jeremy Davis Fri, 16 Aug 2024 12:44:58 +0000 + turnkey-gitlab-18.0 (1) turnkey; urgency=low * Install latest GitLab-CE v16.7.7 - from third party GitLab apt repo. diff --git a/overlay/usr/lib/confconsole/plugins.d/Lets_Encrypt/get_certificate.py b/overlay/usr/lib/confconsole/plugins.d/Lets_Encrypt/get_certificate.py index 1993314..e40668b 100755 --- a/overlay/usr/lib/confconsole/plugins.d/Lets_Encrypt/get_certificate.py +++ b/overlay/usr/lib/confconsole/plugins.d/Lets_Encrypt/get_certificate.py @@ -2,7 +2,6 @@ import requests import subprocess -from os import path, remove # import inithooks_cache (from absolute path) for managing domain caching import sys @@ -22,45 +21,52 @@ support for Let's Encrypt. For more details, please see: -https://docs.gitlab.com/omnibus/settings/ssl.html +https://docs.gitlab.com/omnibus/settings/ssl/ """ + example_domain = 'www.example.com' # XXX Debug paths -def load_domain(): + +def load_domain() -> str: ''' Loads domain from inithooks cache ''' - return inithooks_cache.read('APP_DOMAIN') + return str(inithooks_cache.read('APP_DOMAIN')) + def save_domain(domain): ''' Saves domain configuration ''' inithooks_cache.write('APP_DOMAIN', domain) + +def strip_schema(url: str) -> str: + '''Return domain with http/https schema stripped''' + if url.startswith('http://'): + return url[7:] + elif url.startswith("https://"): + return url[8:] + return url + + def invalid_domain(domain): ''' Validates well known limitations of domain-name specifications doesn't enforce when or if special characters are valid. Returns a string if domain is invalid explaining why otherwise returns False''' if domain == '': - return ('Error: A domain must be provided in {} (with no' - ' preceeding space)'.format(domain_path)) + return 'Error: A domain must be provided' if len(domain) != 0: if len(domain) > 254: - return ('Error in {}: Domain name must not exceed 254' - ' characters'.format(domain)) + return 'Error: Domain must not exceed 254 characters' for part in domain.split('.'): if not 0 < len(part) < 64: - return ('Error in {}: Domain segments may not be larger' - ' than 63 characters or less than 1'.format(domain)) + return ('Error: Domain segments may not be larger than 63' + ' characters or less than 1') return False -def uncomment(file_name, search_term): - '''Dirty function that leverages sed.''' - subprocess.run(["sed", "-i", "/{}/ s|^# *||".format(search_term), filename]) def run(): field_width = 60 - field_name = 'domain' canceled = False @@ -69,7 +75,7 @@ def run(): response = requests.get(LE_INFO_URL) tos_url = response.json()['meta']['termsOfService'] except requests.exceptions.RequestException as e: - msg = "Failed to connect get data from '{}': '{}'".format(LE_INFO_URL, e) + msg = f"Failed to connect get data from '{LE_INFO_URL}': '{e}'" if not tos_url: console.msgbox('Error', msg, autosize=True) return @@ -85,34 +91,26 @@ def run(): return ret = console.yesno( - "Before getting a Let's Encrypt certificate, you must agree " - 'to the current Terms of Service.\n\n' - 'You can find the current Terms of Service here:\n\n' - +tos_url+'\n\n' - "Do you agree to the Let's Encrypt Terms of Service?", + "Before getting a Let's Encrypt certificate, you must agree to the" + " current Terms of Service." + f"\n\nYou can find the current Terms of Service here: \n\n{tos_url}" + "\n\nDo you agree to the Let's Encrypt Terms of Service?", autosize=True ) if ret != 'ok': return - domain = load_domain() - m = invalid_domain(domain) - - if m: - ret = console.yesno( - (str(m) + '\n\nWould you like to ignore and overwrite data?')) - if ret == 'ok': - remove(domain_path) - domain = load_domain() - else: - return - - value = domain + # should have a cached valid domain from firstboot + domain = strip_schema(load_domain()) + # but double check and use example if not + if invalid_domain(domain): + domain = example_domain + domain = f"https://{domain}" while True: while True: field = [ - ('Domain', 1, 0, value, 1, 10, field_width, 255), + ('Domain', 1, 0, domain, 1, 10, field_width, 255), ] ret, value = console.form(TITLE, DESC, field, autosize=True) if len(value) >= 1: @@ -127,7 +125,8 @@ def run(): continue if ret == 'ok': - ret2 = console.yesno('This will overwrite previous settings and check for certificate, continue?') + ret2 = console.yesno("This will overwrite previous settings" + " and check for certificate, continue?") if ret2 == 'ok': save_domain(value) break @@ -136,20 +135,35 @@ def run(): break config = "/etc/gitlab/gitlab.rb" - domain = "https://{}".format(domain) - subprocess.run(["sed", "-i", "/^external_url/ s|'.*|'{}'|".format(domain), config]) - subprocess.run(["sed", "-i", "/letsencrypt\['enable'\]/ s|^# *||", config]) - subprocess.run(["sed", "-i", "/^letsencrypt\['enable'\]/ s|=.*|= true|", config]) - subprocess.run(["sed", "-i", "/letsencrypt\['auto_renew'\]/ s|^# *||", config]) - subprocess.run(["sed", "-i", "/^letsencrypt\['auto_renew'\]/ s|=.*|= true|", config]) + # should be https already - but ensure it + domain = f"https://{strip_schema(domain)}" + + subprocess.run(["sed", "-i", + f"/^external_url/ s|'.*|'{domain}'|", config]) + subprocess.run(["sed", "-i", + r"/letsencrypt\['enable'\]/ s|^# *||", config]) + subprocess.run(["sed", "-i", + r"/^letsencrypt\['enable'\]/ s|=.*|= true|", config]) + subprocess.run(["sed", "-i", + r"/letsencrypt\['auto_renew'\]/ s|^# *||", config]) + subprocess.run(["sed", "-i", + r"/^letsencrypt\['auto_renew'\]/ s|=.*|= true|", + config]) print('Running gitlab-ctl reconfigure. This might take a while...') exit_code = subprocess.run(['gitlab-ctl', 'reconfigure']).returncode if exit_code != 0: - console.msgbox('GitLab Error!', 'Something went wrong!\nPlease check that ' - 'the domain {} resolves to a publicly accessable IP address for this ' - 'server and that ports 80 and 443 are publicly accessible.\n\n' - 'It is also possible that there is some other issue with your config ' - 'file ({}).\n\nFor full details, please try running \'gitlab-ctl ' - 'reconfigure\' from the commandline.\n\nAlso see:\n' - 'https://docs.gitlab.com/omnibus/settings/ssl.html'.format(domain, config)) + console.msgbox( + "GitLab Error!", + "Something went wrong! :(" + f"\n\nPlease check that the domain {domain} resolves to a" + " publicly accessable IP address for this server and that" + " ports 80 and 443 are publicly accessible." + "\n\nIt is also possible that there is some other issue with" + f" your config file ({config})." + "\n\nFor full details, please try running 'gitlab-ctl" + " reconfigure' from the commandline." + "\n\nAlso see:\n" + "\nhttps://docs.gitlab.com/omnibus/settings/ssl/") + else: + save_domain(domain) diff --git a/overlay/usr/lib/inithooks/bin/gitlab.py b/overlay/usr/lib/inithooks/bin/gitlab.py index 0103e31..ac3147f 100755 --- a/overlay/usr/lib/inithooks/bin/gitlab.py +++ b/overlay/usr/lib/inithooks/bin/gitlab.py @@ -1,23 +1,26 @@ #!/usr/bin/python3 -"""Set GitLab root (admin) password, email and domain to serve +"""Set GitLab root user password, email and domain to serve Option: --pass= unless provided, will ask interactively --email= unless provided, will ask interactively --domain= unless provided, will ask interactively - (can include schema) + - can include schema DEFAULT=www.example.com + --schema= unless provided will default to 'http' + - ignored if domain includes schema + """ import sys import getopt from libinithooks import inithooks_cache -import os -import pwd from subprocess import run, Popen, PIPE from libinithooks.dialog_wrapper import Dialog +DEFAULT_DOMAIN = "www.example.com" + def usage(s=None): if s: @@ -26,7 +29,6 @@ def usage(s=None): print(__doc__, file=sys.stderr) sys.exit(1) -DEFAULT_DOMAIN = "www.example.com" def main(): try: @@ -35,9 +37,10 @@ def main(): except getopt.GetoptError as e: usage(e) + password = "" email = "" domain = "" - password = "" + schema = "" for opt, val in opts: if opt in ('-h', '--help'): usage() @@ -55,7 +58,7 @@ def main(): password = d.get_password( "GitLab Password", "Enter new password for the GitLab 'root' account.", - pass_req = 8) + pass_req=8) if not email: if 'd' not in locals(): @@ -81,21 +84,32 @@ def main(): domain = DEFAULT_DOMAIN inithooks_cache.write('APP_DOMAIN', domain) - + print("Reconfiguring GitLab. This might take a while.") config = "/etc/gitlab/gitlab.rb" - domain = "http://%s" % domain - run(["sed", "-i", "/^external_url/ s|'.*|'%s'|" % domain, config]) - run(["sed", "-i", "/^gitlab_rails\['gitlab_email_from'\]/ s|=.*|= '%s'|" % email, config]) + + if not domain.startswith('http'): + if schema: + if not schema.endswith("://"): + schema = f"{schema}://" + domain = f"{schema}{domain}" + else: + domain = f"http://{domain}" + run(["sed", "-i", f"/^external_url/ s|'.*|'{domain}'|", config]) + run(["sed", "-i", + fr"/^gitlab_rails\['gitlab_email_from'\]/ s|=.*|= '{email}'|", + config]) run(["gitlab-ctl", "reconfigure"]) print("Setting GitLab 'root' user password. This might take a while.") - p1 = Popen(["echo", "-e", "{}\n{}\n".format(password, password)], stdout=PIPE) - p2 = Popen(["gitlab-rake", "gitlab:password:reset[root]"], stdin=p1.stdout, stdout=PIPE) + p1 = Popen(["echo", "-e", f"{password}\n{password}\n"], stdout=PIPE) + p2 = Popen(["gitlab-rake", "gitlab:password:reset[root]"], + stdin=p1.stdout, stdout=PIPE) p1.stdout.close() output = p2.communicate()[0] print(output) sys.exit(p2.returncode) + if __name__ == "__main__": main()