Add managed crontab support
This commit is contained in:
parent
53740655b6
commit
312e231ea6
11
README.md
11
README.md
|
@ -41,17 +41,20 @@ intake
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"...": "..."
|
"...": "..."
|
||||||
}
|
},
|
||||||
|
"cron": "* * * * *"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Each key under `action` defines an action that can be taken for the source. `action` must be present with a `fetch` action. `env` is optional.
|
Each key under `action` defines an action that can be taken for the source. A minimal `intake.json` must contain a `fetch` action with an `exe`. All other configs are optional.
|
||||||
|
|
||||||
|
The `fetch` action is executed by `intake update`. All other actions are executable with `intake action`. `intake crontab` will create a crontab entry for each source with a `cron` config defined. The value of `cron` is the crontab spec according to which the source should be updated.
|
||||||
|
|
||||||
## Interface for source programs
|
## Interface for source programs
|
||||||
|
|
||||||
Intake interacts with sources by executing the actions defined in the source's `intake.json`. The `fetch` action is required and used to check for new feed items.
|
Intake interacts with sources by executing the actions defined in the source's `intake.json`. The `fetch` action is required and used to check for new feed items when `intake update` is executed.
|
||||||
|
|
||||||
When any action is executed, intake executes the `exe` program for the action with the corresponding `args` as arguments. The process's working directory is set to the source's folder, i.e. the folder containing `intake.json`. The process's environment is as follows:
|
To execute an action, intake executes the `exe` program for the action with the corresponding `args` (if present) as arguments. The process's working directory is set to the source's folder, i.e. the folder containing `intake.json`. The process's environment is as follows:
|
||||||
|
|
||||||
* intake's environment is inherited.
|
* intake's environment is inherited.
|
||||||
* `STATE_PATH` is set to the absolute path of `state`.
|
* `STATE_PATH` is set to the absolute path of `state`.
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
"exe": "currenttime.sh",
|
"exe": "currenttime.sh",
|
||||||
"args": []
|
"args": []
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"cron": "* * * * *"
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,16 @@
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
password = "alpha";
|
password = "alpha";
|
||||||
uid = 1000;
|
uid = 1000;
|
||||||
|
packages = [ pkgs.intake ];
|
||||||
};
|
};
|
||||||
|
|
||||||
users.users.bob = {
|
users.users.bob = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
password = "beta";
|
password = "beta";
|
||||||
uid = 1001;
|
uid = 1001;
|
||||||
|
packages = [ pkgs.intake ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Put intake on both users' PATH
|
|
||||||
environment.systemPackages = [ pkgs.intake ];
|
|
||||||
|
|
||||||
# Set up intake for both users with an entry point at port 8080
|
# Set up intake for both users with an entry point at port 8080
|
||||||
services.intake = {
|
services.intake = {
|
||||||
listen.port = 8080;
|
listen.port = 8080;
|
||||||
|
|
|
@ -7,7 +7,16 @@ import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from flask import Flask, render_template, request, jsonify, abort, redirect, url_for, current_app
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
jsonify,
|
||||||
|
abort,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
current_app,
|
||||||
|
)
|
||||||
|
|
||||||
from intake.core import intake_data_dir
|
from intake.core import intake_data_dir
|
||||||
from intake.source import LocalSource, execute_action, Item
|
from intake.source import LocalSource, execute_action, Item
|
||||||
|
|
|
@ -11,11 +11,11 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from intake.core import intake_data_dir
|
from intake.core import intake_data_dir
|
||||||
|
from intake.crontab import update_crontab_entries
|
||||||
from intake.source import fetch_items, LocalSource, update_items, execute_action
|
from intake.source import fetch_items, LocalSource, update_items, execute_action
|
||||||
from intake.types import InvalidConfigException, SourceUpdateException
|
from intake.types import InvalidConfigException, SourceUpdateException
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_edit(cmd_args):
|
def cmd_edit(cmd_args):
|
||||||
"""Open a source's config for editing."""
|
"""Open a source's config for editing."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
@ -296,6 +296,24 @@ def cmd_passwd(cmd_args):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_crontab(cmd_args):
|
||||||
|
"""Update cron with configured source cron entries."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="intake crontab",
|
||||||
|
description=cmd_crontab.__doc__,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--data",
|
||||||
|
"-d",
|
||||||
|
help="Path to the intake data directory containing source directories",
|
||||||
|
)
|
||||||
|
args = parser.parse_args(cmd_args)
|
||||||
|
|
||||||
|
data_path: Path = Path(args.data) if args.data else intake_data_dir()
|
||||||
|
update_crontab_entries(data_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cmd_run(cmd_args):
|
def cmd_run(cmd_args):
|
||||||
"""Run the default Flask server."""
|
"""Run the default Flask server."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from intake.source import LocalSource
|
||||||
|
|
||||||
|
|
||||||
|
INTAKE_CRON_BEGIN = "### begin intake-managed crontab entries"
|
||||||
|
INTAKE_CRON_END = "### end intake-managed crontab entries"
|
||||||
|
|
||||||
|
|
||||||
|
def get_desired_crons(data_path: Path):
|
||||||
|
"""
|
||||||
|
Get a list of sources and crontab specs from the data directory.
|
||||||
|
"""
|
||||||
|
for child in data_path.iterdir():
|
||||||
|
if not (child / "intake.json").exists():
|
||||||
|
continue
|
||||||
|
source = LocalSource(data_path, child.name)
|
||||||
|
config = source.get_config()
|
||||||
|
if cron := config.get("cron"):
|
||||||
|
yield f"{cron} . /etc/profile; intake update -s {source.source_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def update_crontab_entries(data_path: Path):
|
||||||
|
"""
|
||||||
|
Update the intake-managed section of the user's crontab.
|
||||||
|
"""
|
||||||
|
# If there is no crontab command available, quit early.
|
||||||
|
crontab_exists = subprocess.run(["command", "-v" "crontab"], shell=True)
|
||||||
|
if crontab_exists.returncode:
|
||||||
|
print("Could not update crontab")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the current crontab
|
||||||
|
get_crontab = subprocess.run(
|
||||||
|
["crontab", "-e"],
|
||||||
|
env={**os.environ, "EDITOR": "cat"},
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
crontab_lines = get_crontab.stdout.decode("utf-8").splitlines()
|
||||||
|
|
||||||
|
# Splice the intake crons into the crontab
|
||||||
|
new_crontab_lines = []
|
||||||
|
section_found = False
|
||||||
|
in_section = False
|
||||||
|
for i in range(len(crontab_lines)):
|
||||||
|
|
||||||
|
if not section_found and crontab_lines[i] == INTAKE_CRON_BEGIN:
|
||||||
|
section_found = True
|
||||||
|
in_section = True
|
||||||
|
# Open the section and add everything
|
||||||
|
new_crontab_lines.append(INTAKE_CRON_BEGIN)
|
||||||
|
new_crontab_lines.extend(get_desired_crons(data_path))
|
||||||
|
|
||||||
|
elif crontab_lines[i] == INTAKE_CRON_END:
|
||||||
|
new_crontab_lines.append(INTAKE_CRON_END)
|
||||||
|
in_section = False
|
||||||
|
|
||||||
|
elif not in_section:
|
||||||
|
new_crontab_lines.append(crontab_lines[i])
|
||||||
|
|
||||||
|
# If the splice mark was never found, append the whole section to the end
|
||||||
|
if not section_found:
|
||||||
|
new_crontab_lines.append(INTAKE_CRON_BEGIN)
|
||||||
|
new_crontab_lines.extend(get_desired_crons(data_path))
|
||||||
|
new_crontab_lines.append(INTAKE_CRON_END)
|
||||||
|
|
||||||
|
# Save the updated crontab
|
||||||
|
new_crontab: bytes = "\n".join(new_crontab_lines).encode("utf8")
|
||||||
|
save_crontab = subprocess.Popen(
|
||||||
|
["crontab", "-"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
(stdout, stderr) = save_crontab.communicate(new_crontab)
|
||||||
|
for line in stdout.decode("utf8").splitlines():
|
||||||
|
print("[stdout]", line)
|
||||||
|
for line in stderr.decode("utf8").splitlines():
|
||||||
|
print("[stderr]", line)
|
|
@ -74,7 +74,7 @@ in {
|
||||||
intakeDir = "/etc/intake";
|
intakeDir = "/etc/intake";
|
||||||
intakePwd = "${intakeDir}/htpasswd";
|
intakePwd = "${intakeDir}/htpasswd";
|
||||||
in {
|
in {
|
||||||
# Apply the overlay so intake is included inpkgs.
|
# Apply the overlay so intake is included in pkgs.
|
||||||
nixpkgs.overlays = [ flake.overlays.default ];
|
nixpkgs.overlays = [ flake.overlays.default ];
|
||||||
|
|
||||||
# Define a user group for access to the htpasswd file. nginx needs to be able to read it.
|
# Define a user group for access to the htpasswd file. nginx needs to be able to read it.
|
||||||
|
@ -105,6 +105,9 @@ in {
|
||||||
};
|
};
|
||||||
in mkMerge (map addPackagesToUser enabledUserNames);
|
in mkMerge (map addPackagesToUser enabledUserNames);
|
||||||
|
|
||||||
|
# Enable cron
|
||||||
|
services.cron.enable = true;
|
||||||
|
|
||||||
# Define a user service for each configured user
|
# Define a user service for each configured user
|
||||||
systemd.services =
|
systemd.services =
|
||||||
let
|
let
|
||||||
|
|
Loading…
Reference in New Issue