Skip to content

Commit a2495ea

Browse files
committed
ref: restructure site configuration workflow logic
1 parent 5f45682 commit a2495ea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+253
-231
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
0.11.0
2+
- ref: restructure site configuration workflow logic
13
0.10.11
24
- enh: only restart nginx/subervisor in 'dcor inspect' when necessary
35
0.10.10

dcor_control/cli/info.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pathlib
12
import socket
23

34
import click
@@ -9,13 +10,16 @@
910
s3 = None
1011

1112

12-
from ..inspect.config_ckan import get_expected_ckan_options, get_ip
13+
from ..inspect.config_ckan import get_expected_site_options, get_ip
14+
from ..util import get_dcor_control_config
1315

1416

1517
@click.command()
1618
def status():
1719
"""Display DCOR status"""
18-
srv_opts = get_expected_ckan_options()
20+
dcor_site_config_dir = pathlib.Path(
21+
get_dcor_control_config("dcor-site-config-dir", interactive=False))
22+
srv_opts = get_expected_site_options(dcor_site_config_dir)
1923
click.secho(f"DCOR installation: '{srv_opts['name']}'", bold=True)
2024
click.echo(f"IP Address: {get_ip()}")
2125
click.echo(f"Hostname: {socket.gethostname()}")

dcor_control/cli/inspect.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pathlib
2+
13
import click
24
from dcor_shared import get_ckan_config_option, paths
35

@@ -6,11 +8,24 @@
68

79
@click.command()
810
@click.option('--assume-yes', is_flag=True)
9-
def inspect(assume_yes=False):
11+
@click.option("--dcor-site-config-dir",
12+
type=click.Path(dir_okay=False,
13+
resolve_path=True,
14+
path_type=pathlib.Path),
15+
help="Path to a custom site configuration. For the main "
16+
"sites in production, dcor_control comes with predefined "
17+
"configurations (see the `resources` directory) and "
18+
"the correct configuration can be inferred from e.g. "
19+
"the hostname or IP address. If you are running a custom "
20+
"DCOR instance, you may pass a path to your own "
21+
"site configuration directory. You may also specify the "
22+
"`DCOR_SITE_CONFIG_DIR` environment variable instead.")
23+
def inspect(assume_yes=False, dcor_site_config_dir=None):
1024
"""Inspect this DCOR installation"""
1125
cn = 0
1226
click.secho("Checking CKAN options...", bold=True)
13-
cn += inspect_mod.check_ckan_ini(autocorrect=assume_yes)
27+
cn += inspect_mod.check_ckan_ini(dcor_site_config_dir=dcor_site_config_dir,
28+
autocorrect=assume_yes)
1429

1530
click.secho("Checking beaker session secret...", bold=True)
1631
cn += inspect_mod.check_ckan_beaker_session_cookie_secret(

dcor_control/inspect/config_ckan.py

Lines changed: 93 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import copy
2+
import functools
23
import json
4+
import os
35
import pathlib
4-
from pkg_resources import resource_filename
56
import socket
67
import subprocess as sp
78
import uuid
89

910
from dcor_shared.paths import get_ckan_config_option, get_ckan_config_path
1011
from dcor_shared.parse import ConfigOptionNotFoundError, parse_ini_config
1112

13+
from ..resources import resource_location
1214
from .. import util
1315

1416
from . import common
@@ -33,25 +35,21 @@ def check_ckan_beaker_session_cookie_secret(autocorrect=False):
3335
return did_something
3436

3537

36-
def check_ckan_ini(autocorrect=False):
38+
def check_ckan_ini(dcor_site_config_dir=None, autocorrect=False):
3739
"""Check custom ckan.ini server options
3840
3941
This includes the contributions from
40-
- general options from resources/dcor_options.ini
42+
- general options from resources/dcor_defaults.ini
4143
- as well as custom options in resources/server_options.json
4244
4345
Custom options override general options.
4446
"""
4547
did_something = 0
46-
custom_opts = get_expected_ckan_options()["ckan.ini"]
47-
general_opts = parse_ini_config(
48-
resource_filename("dcor_control.resources", "dcor_options.ini"))
48+
dcor_opts = get_expected_site_options(dcor_site_config_dir)["ckan.ini"]
4949

50-
general_opts.update(custom_opts)
51-
52-
for key in general_opts:
50+
for key in dcor_opts:
5351
did_something += check_ckan_ini_option(
54-
key, general_opts[key], autocorrect=autocorrect)
52+
key, dcor_opts[key], autocorrect=autocorrect)
5553

5654
return did_something
5755

@@ -147,7 +145,7 @@ def check_dcor_theme_main_css(autocorrect):
147145
did_something = 0
148146
ckan_ini = get_ckan_config_path()
149147
opt = get_actual_ckan_option("ckan.theme")
150-
# TODO: Check whether the paths created by this script are setup correctly
148+
# TODO: Check whether the paths created by this script are set up correctly
151149
if opt != "dcor_theme_main/dcor_theme_main":
152150
if autocorrect:
153151
print("Applying DCOR theme main css")
@@ -175,107 +173,103 @@ def get_actual_ckan_option(key):
175173
return opt
176174

177175

178-
def get_expected_ckan_options():
179-
"""Return expected ckan.ini options for the current host"""
180-
# Load the json data
181-
opt_path = resource_filename("dcor_control.resources",
182-
"server_options.json")
183-
with open(opt_path) as fd:
184-
opt_dict = json.load(fd)
185-
# Determine which server we are on
186-
my_hostname = socket.gethostname()
187-
my_ip = get_ip()
176+
def get_dcor_site_config_dir(dcor_site_config_dir=None):
177+
"""Return a local directory on disk containing the site's configuration
188178
189-
cands = []
190-
for setup in opt_dict["setups"]:
191-
req = setup["requirements"]
192-
ip = req.get("ip", "")
193-
if ip == "unknown":
194-
# The IP is unknown for this server.
195-
ip = my_ip
196-
hostname = req.get("hostname", "")
197-
if ip == my_ip and hostname == my_hostname:
198-
# perfect match
199-
cands = [setup]
200-
break
201-
elif ip or hostname:
202-
# no match
203-
continue
204-
else:
205-
# fallback setup
206-
cands.append(setup)
207-
if len(cands) == 0:
208-
raise ValueError("No fallback setups?")
209-
if len(cands) != 1:
210-
names = [setup["name"] for setup in cands]
211-
custom_message = "Valid setup-identifiers: {}".format(
212-
", ".join(names))
213-
for _ in range(3):
214-
sn = util.get_dcor_control_config("setup-identifier",
215-
custom_message)
216-
if sn is not None:
179+
The configuration directory is searched for in the following order:
180+
181+
1. Path passed in dcor_site_config_dir
182+
2. Environment variable `DCOR_SITE_CONFIG_DIR`
183+
3. Matching sites in the `dcor_control.resources` directory
184+
"""
185+
if dcor_site_config_dir is not None:
186+
# passed via argument
187+
pass
188+
elif (env_cfg_dir := os.environ.get("DCOR_SITE_CONFIG_DIR")) is not None:
189+
# environment variable
190+
dcor_site_config_dir = env_cfg_dir
191+
else:
192+
# search registered sites
193+
for site_dir in sorted(resource_location.glob("site_dcor-*")):
194+
if is_site_config_dir_applicable(site_dir):
195+
dcor_site_config_dir = site_dir
217196
break
218197
else:
219-
raise ValueError("Could not get setup-identifier (tried 3 times)!")
220-
setup = cands[names.index(sn)]
221-
else:
222-
setup = cands[0]
198+
raise ValueError(
199+
"Could not determine the DCOR site configuration. Please "
200+
"specify the `dcor_site_config_dir` keyword argument or "
201+
"set the `DCOR_SITE_CONFIG_DIR` environment variable.")
202+
if not is_site_config_dir_applicable(dcor_site_config_dir):
203+
raise ValueError(
204+
f"The site configuration directory '{dcor_site_config_dir}' is "
205+
f"not applicable. Please check hostname and IP address.")
206+
207+
return dcor_site_config_dir
208+
209+
210+
def get_expected_site_options(dcor_site_config_dir):
211+
"""Return expected site config options for the specified site
212+
213+
Returns a dictionary with "name", "requirements", and "ckan.ini".
214+
"""
215+
dcor_site_config_dir = get_dcor_site_config_dir(dcor_site_config_dir)
216+
cfg = json.loads((dcor_site_config_dir / "dcor_config.json").read_text())
217+
cfg["dcor_site_config_dir"] = dcor_site_config_dir
218+
# Store the information into permanent storage. We might reuse it.
219+
util.set_dcor_control_config("setup-identifier", cfg["name"])
220+
util.set_dcor_control_config("dcor-site-config-dir",
221+
str(dcor_site_config_dir))
222+
223+
# Import DCOR default ckan.ini variables
224+
cfg_d = parse_ini_config(resource_location / "dcor_defaults.ini.template")
225+
for key, value in cfg_d.items():
226+
cfg["ckan.ini"].setdefault(key, value)
223227

224-
# Populate with includes
225-
for inc_key in setup["include"]:
226-
common.recursive_update_dict(setup, opt_dict["includes"][inc_key])
227228
# Fill in template variables
228-
update_expected_ckan_options_templates(setup)
229-
# Fill in branding variables
230-
update_expected_ckan_options_branding(setup)
231-
return setup
232-
233-
234-
def update_expected_ckan_options_branding(ini_dict):
235-
"""Update dict with templates and public paths according to branding"""
236-
brands = ini_dict["branding"]
237-
# Please not the dcor_control must be an installed package for
238-
# this to work (no egg or somesuch).
239-
templt_paths = []
240-
public_paths = []
241-
for brand in brands:
242-
template_dir = resource_filename("dcor_control.resources.branding",
243-
"templates_{}".format(brand))
244-
if pathlib.Path(template_dir).exists():
245-
templt_paths.append(template_dir)
246-
public_dir = resource_filename("dcor_control.resources.branding",
247-
"public_{}".format(brand))
248-
if pathlib.Path(public_dir).exists():
249-
public_paths.append(public_dir)
250-
if templt_paths:
251-
ini_dict["ckan.ini"]["extra_template_paths"] = ", ".join(templt_paths)
252-
if public_paths:
253-
ini_dict["ckan.ini"]["extra_public_paths"] = ", ".join(public_paths)
254-
255-
256-
def update_expected_ckan_options_templates(ini_dict):
229+
update_expected_ckan_options_templates(cfg)
230+
231+
return cfg
232+
233+
234+
@functools.lru_cache()
235+
def is_site_config_dir_applicable(dcor_site_config_dir):
236+
cfg = json.loads((dcor_site_config_dir / "dcor_config.json").read_text())
237+
# Determine which server we are on
238+
my_hostname = socket.gethostname()
239+
my_ip = get_ip()
240+
241+
req = cfg["requirements"]
242+
ip = req.get("ip", "")
243+
if ip == "unknown":
244+
# The IP is unknown for this server.
245+
ip = my_ip
246+
hostname = req.get("hostname", "")
247+
return ip == my_ip and hostname == my_hostname
248+
249+
250+
def update_expected_ckan_options_templates(cfg_dict, templates=None):
257251
"""Update dict with templates in server_options.json"""
258-
templates = {
259-
"IP": [get_ip, []],
260-
"EMAIL": [util.get_dcor_control_config, ["email"]],
261-
"PGSQLPASS": [util.get_dcor_control_config, ["pgsqlpass"]],
262-
"HOSTNAME": [socket.gethostname, []],
263-
"PATH_BRANDING": [resource_filename, ["dcor_control.resources",
264-
"branding"]],
265-
}
266-
267-
for key in sorted(ini_dict.keys()):
268-
item = ini_dict[key]
252+
if templates is None:
253+
templates = {
254+
"IP": [get_ip, []],
255+
"EMAIL": [util.get_dcor_control_config, ["email"]],
256+
"PGSQLPASS": [util.get_dcor_control_config, ["pgsqlpass"]],
257+
"HOSTNAME": [socket.gethostname, []],
258+
"DCOR_SITE_CONFIG_DIR": [cfg_dict.get, ["dcor_site_config_dir"]],
259+
}
260+
261+
for key in sorted(cfg_dict.keys()):
262+
item = cfg_dict[key]
269263
if isinstance(item, str):
270264
for tk in templates:
271265
tstr = "<TEMPLATE:{}>".format(tk)
272266
if item.count(tstr):
273267
func, args = templates[tk]
274-
item = item.replace(tstr, func(*args))
275-
ini_dict[key] = item
268+
item = item.replace(tstr, str(func(*args)))
269+
cfg_dict[key] = item
276270
elif isinstance(item, dict):
277271
# recurse into nested dicts
278-
update_expected_ckan_options_templates(item)
272+
update_expected_ckan_options_templates(item, templates=templates)
279273

280274

281275
def get_ip():

dcor_control/resources/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pathlib
2+
3+
resource_location = pathlib.Path(__file__).parent.resolve()

dcor_control/resources/branding/__init__.py

Whitespace-only changes.

dcor_control/resources/dcor_options.ini renamed to dcor_control/resources/dcor_defaults.ini.template

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
# This is a template file. Any option defined via <TEMPLATE:TAGS> must be
2+
# replaced with the corresponding value.
3+
14
# These are general CKAN configuration options for DCOR from which
25
# individual package options are derived. To make site-specific
3-
# changes, edit server_options.json, which overrides options in this
4-
# file here.
5-
#
6-
# Run `ckan config-tool /etc/ckan/default/ckan.ini -f dcor_options.ini`
7-
# to apply these options. If you also want site-specific options to be
8-
# taken into account, run `dcor inspect`.
6+
# changes, you can specify a `config_dcor.json` file when running
7+
# `dcor inspect`, which overrides options in the present file.
8+
9+
# General CKAN tweaks
10+
ckan.site_title = DCOR
11+
ckan.storage_path = /data/ckan-<TEMPLATE:HOSTNAME>
912

1013
# authorization
1114
ckan.auth.anon_create_dataset = false
@@ -20,7 +23,7 @@ ckan.auth.roles_that_cascade_to_sub_groups = admin
2023
ckan.auth.public_user_details = false
2124
ckan.auth.allow_dataset_collaborators = true
2225
ckan.auth.create_user_via_api = false
23-
ckan.auth.create_user_via_web = true
26+
ckan.auth.create_user_via_web = false
2427

2528
# uploads
2629
# only allow image types for user avatars and group images
@@ -37,6 +40,9 @@ dcor_object_store.ssl_verify = true
3740
# are stored in the "RES/OUR/CEID-SCHEME" in that bucket.
3841
dcor_object_store.bucket_name = circle-{organization_id}
3942

43+
# postgresql
44+
sqlalchemy.url = postgresql://ckan_default:<TEMPLATE:PGSQLPASS>@localhost/ckan_default
45+
4046
# search
4147
ckan.search.default_include_private = true
4248

@@ -65,9 +71,11 @@ ckanext.dc_serve.tmp_dir = /data/tmp/ckanext-dc_serve
6571

6672
# ckanext-dcor_depot
6773
ckanext.dcor_depot.depots_path = /data/depots
74+
ckanext.dcor_depot.users_depot_name = users-<TEMPLATE:HOSTNAME>
6875

6976
# ckanext-dcor_schemas
7077
ckan.extra_resource_fields = etag sha256
78+
extra_template_paths = file://<TEMPLATE:DCOR_SITE_CONFIG_DIR>/templates
7179
ckanext.dcor_schemas.allow_public_datasets = true
7280
ckanext.dcor_schemas.json_resource_schema_dir = package
7381

@@ -81,6 +89,7 @@ beaker.session.cookie_expires = 7776000
8189
beaker.session.crypto_type = cryptography
8290
# Optimal would be "json", but there were issues with `datetime`
8391
# objects that could not be jsonified.
92+
beaker.session.domain = <TEMPLATE:HOSTNAME>
8493
beaker.session.data_serializer = pickle
8594
beaker.session.httponly = true
8695
beaker.session.samesite = Strict

0 commit comments

Comments
 (0)