Improve job running in local and k8s
Running jobs was previously done with "exec". This was because it
allowed us to avoid copying too much container specification information
from the docker-compose/deployments files to the jobs files. However,
this was limiting:
- In order to run a job, the corresponding container had to be running.
This was particularly painful in Kubernetes, where containers are
crashing as long as migrations are not correctly run.
- Containers in which we need to run jobs needed to be present in the
docker-compose/deployments files. This is unnecessary, for example when
mysql is disabled, or in the case of the certbot container.
Now, we create dedicated jobs files, both for local and k8s deployment.
This introduces a little redundancy, but not too much. Note that
dependent containers are not listed in the docker-compose.jobs.yml file,
so an actual platform is still supposed to be running when we launch the
jobs.
This also introduces a subtle change: now, jobs go through the container
entrypoint prior to running. This is probably a good thing, as it will
avoid forgetting about incorrect environment variables.
In k8s, we find ourselves interacting way too much with the kubectl
utility. Parsing output from the CLI is a pain. So we need to switch to
the native kubernetes client library.
2020-03-25 17:47:36 +00:00
|
|
|
from datetime import datetime
|
|
|
|
from time import sleep
|
2021-04-06 10:09:00 +00:00
|
|
|
from typing import Any, List, Optional, Type
|
Improve job running in local and k8s
Running jobs was previously done with "exec". This was because it
allowed us to avoid copying too much container specification information
from the docker-compose/deployments files to the jobs files. However,
this was limiting:
- In order to run a job, the corresponding container had to be running.
This was particularly painful in Kubernetes, where containers are
crashing as long as migrations are not correctly run.
- Containers in which we need to run jobs needed to be present in the
docker-compose/deployments files. This is unnecessary, for example when
mysql is disabled, or in the case of the certbot container.
Now, we create dedicated jobs files, both for local and k8s deployment.
This introduces a little redundancy, but not too much. Note that
dependent containers are not listed in the docker-compose.jobs.yml file,
so an actual platform is still supposed to be running when we launch the
jobs.
This also introduces a subtle change: now, jobs go through the container
entrypoint prior to running. This is probably a good thing, as it will
avoid forgetting about incorrect environment variables.
In k8s, we find ourselves interacting way too much with the kubectl
utility. Parsing output from the CLI is a pain. So we need to switch to
the native kubernetes client library.
2020-03-25 17:47:36 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
import click
|
|
|
|
|
2022-01-07 16:58:01 +00:00
|
|
|
from tutor import config as tutor_config
|
|
|
|
from tutor import env as tutor_env
|
2022-10-19 15:46:31 +00:00
|
|
|
from tutor import exceptions, fmt, hooks
|
2022-04-18 22:33:55 +00:00
|
|
|
from tutor import interactive as interactive_config
|
2022-10-18 14:57:07 +00:00
|
|
|
from tutor import serialize, utils
|
|
|
|
from tutor.commands import jobs
|
2022-01-07 16:58:01 +00:00
|
|
|
from tutor.commands.config import save as config_save_command
|
2022-10-19 15:46:31 +00:00
|
|
|
from tutor.commands.context import BaseTaskContext
|
2022-12-12 22:28:22 +00:00
|
|
|
from tutor.commands.upgrade import OPENEDX_RELEASE_NAMES
|
2023-01-06 18:02:17 +00:00
|
|
|
from tutor.commands.upgrade.k8s import upgrade_from
|
2022-10-19 15:46:31 +00:00
|
|
|
from tutor.tasks import BaseTaskRunner
|
2022-01-07 16:58:01 +00:00
|
|
|
from tutor.types import Config, get_typed
|
2021-02-25 08:09:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
class K8sClients:
|
|
|
|
_instance = None
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
# Loading the kubernetes module here to avoid import overhead
|
2022-10-19 15:46:31 +00:00
|
|
|
# pylint: disable=import-outside-toplevel
|
|
|
|
from kubernetes import client, config
|
2021-02-25 08:09:14 +00:00
|
|
|
|
|
|
|
config.load_kube_config()
|
|
|
|
self._batch_api = None
|
|
|
|
self._core_api = None
|
|
|
|
self._client = client
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def instance(cls: Type["K8sClients"]) -> "K8sClients":
|
|
|
|
if cls._instance is None:
|
|
|
|
cls._instance = cls()
|
|
|
|
return cls._instance
|
|
|
|
|
|
|
|
@property
|
|
|
|
def batch_api(self): # type: ignore
|
|
|
|
if self._batch_api is None:
|
|
|
|
self._batch_api = self._client.BatchV1Api()
|
|
|
|
return self._batch_api
|
|
|
|
|
|
|
|
@property
|
|
|
|
def core_api(self): # type: ignore
|
|
|
|
if self._core_api is None:
|
|
|
|
self._core_api = self._client.CoreV1Api()
|
|
|
|
return self._core_api
|
|
|
|
|
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
class K8sTaskRunner(BaseTaskRunner):
|
|
|
|
"""
|
|
|
|
Run tasks (bash commands) in Kubernetes-managed services.
|
2021-02-25 08:09:14 +00:00
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
Note: a single Tutor "task" correspond to a Kubernetes "job":
|
|
|
|
https://kubernetes.io/docs/concepts/workloads/controllers/job/
|
|
|
|
A Tutor "job" is composed of multiple Tutor tasks run in different services.
|
|
|
|
|
|
|
|
In Kubernetes, each task that is expected to run in a "myservice" container will
|
|
|
|
trigger the "myservice-job" Kubernetes job. This job definition must be present in
|
|
|
|
the "k8s/jobs.yml" template.
|
|
|
|
"""
|
2021-02-25 08:09:14 +00:00
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
def run_task(self, service: str, command: str) -> int:
|
2022-01-08 10:24:50 +00:00
|
|
|
job_name = f"{service}-job"
|
2021-09-06 14:20:36 +00:00
|
|
|
job = self.load_job(job_name)
|
2021-02-25 08:09:14 +00:00
|
|
|
# Create a unique job name to make it deduplicate jobs and make it easier to
|
|
|
|
# find later. Logs of older jobs will remain available for some time.
|
|
|
|
job_name += "-" + datetime.now().strftime("%Y%m%d%H%M%S")
|
|
|
|
|
|
|
|
# Wait until all other jobs are completed
|
|
|
|
while True:
|
|
|
|
active_jobs = self.active_job_names()
|
|
|
|
if not active_jobs:
|
|
|
|
break
|
|
|
|
fmt.echo_info(
|
2022-01-08 10:24:50 +00:00
|
|
|
f"Waiting for active jobs to terminate: {' '.join(active_jobs)}"
|
2021-02-25 08:09:14 +00:00
|
|
|
)
|
|
|
|
sleep(5)
|
|
|
|
|
|
|
|
# Configure job
|
|
|
|
job["metadata"]["name"] = job_name
|
|
|
|
job["metadata"].setdefault("labels", {})
|
|
|
|
job["metadata"]["labels"]["app.kubernetes.io/name"] = job_name
|
2021-07-11 14:16:54 +00:00
|
|
|
# Define k8s entrypoint/args
|
|
|
|
shell_command = ["sh", "-e", "-c"]
|
|
|
|
if job["spec"]["template"]["spec"]["containers"][0].get("command") == []:
|
|
|
|
# In some cases, we need to bypass the container entrypoint.
|
|
|
|
# Unfortunately, AFAIK, there is no way to do so in K8s manifests. So we mark
|
|
|
|
# some jobs with "command: []". For these jobs, the entrypoint becomes "sh -e -c".
|
|
|
|
# We do not do this for every job, because some (most) entrypoints are actually useful.
|
|
|
|
job["spec"]["template"]["spec"]["containers"][0]["command"] = shell_command
|
|
|
|
container_args = [command]
|
|
|
|
else:
|
|
|
|
container_args = shell_command + [command]
|
|
|
|
job["spec"]["template"]["spec"]["containers"][0]["args"] = container_args
|
2021-02-25 08:09:14 +00:00
|
|
|
job["spec"]["backoffLimit"] = 1
|
|
|
|
job["spec"]["ttlSecondsAfterFinished"] = 3600
|
|
|
|
# Save patched job to "jobs.yml" file
|
2022-01-08 10:24:50 +00:00
|
|
|
with open(
|
|
|
|
tutor_env.pathjoin(self.root, "k8s", "jobs.yml"), "w", encoding="utf-8"
|
|
|
|
) as job_file:
|
2021-02-25 08:09:14 +00:00
|
|
|
serialize.dump(job, job_file)
|
|
|
|
# We cannot use the k8s API to create the job: configMap and volume names need
|
|
|
|
# to be found with the right suffixes.
|
2022-02-21 10:53:17 +00:00
|
|
|
kubectl_apply(
|
2022-03-17 09:34:17 +00:00
|
|
|
self.root,
|
2021-02-25 08:09:14 +00:00
|
|
|
"--selector",
|
2022-01-08 10:24:50 +00:00
|
|
|
f"app.kubernetes.io/name={job_name}",
|
2021-02-25 08:09:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
message = (
|
|
|
|
"Job {job_name} is running. To view the logs from this job, run:\n\n"
|
|
|
|
""" kubectl logs --namespace={namespace} --follow $(kubectl get --namespace={namespace} pods """
|
|
|
|
"""--selector=job-name={job_name} -o=jsonpath="{{.items[0].metadata.name}}")\n\n"""
|
|
|
|
"Waiting for job completion..."
|
2021-04-06 10:09:00 +00:00
|
|
|
).format(job_name=job_name, namespace=k8s_namespace(self.config))
|
2021-02-25 08:09:14 +00:00
|
|
|
fmt.echo_info(message)
|
|
|
|
|
|
|
|
# Wait for completion
|
2022-01-08 10:24:50 +00:00
|
|
|
field_selector = f"metadata.name={job_name}"
|
2021-02-25 08:09:14 +00:00
|
|
|
while True:
|
|
|
|
namespaced_jobs = K8sClients.instance().batch_api.list_namespaced_job(
|
2021-06-03 16:12:52 +00:00
|
|
|
k8s_namespace(self.config), field_selector=field_selector
|
2021-02-25 08:09:14 +00:00
|
|
|
)
|
|
|
|
if not namespaced_jobs.items:
|
|
|
|
continue
|
|
|
|
job = namespaced_jobs.items[0]
|
|
|
|
if not job.status.active:
|
|
|
|
if job.status.succeeded:
|
2022-01-08 10:24:50 +00:00
|
|
|
fmt.echo_info(f"Job {job_name} successful.")
|
2021-02-25 08:09:14 +00:00
|
|
|
break
|
|
|
|
if job.status.failed:
|
|
|
|
raise exceptions.TutorError(
|
2022-01-08 10:24:50 +00:00
|
|
|
f"Job {job_name} failed. View the job logs to debug this issue."
|
2021-02-25 08:09:14 +00:00
|
|
|
)
|
|
|
|
sleep(5)
|
|
|
|
return 0
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
def load_job(self, name: str) -> Any:
|
|
|
|
"""
|
|
|
|
Find a given job definition in the rendered k8s/jobs.yml template.
|
|
|
|
"""
|
|
|
|
all_jobs = self.render("k8s", "jobs.yml")
|
|
|
|
for job in serialize.load_all(all_jobs):
|
|
|
|
job_name = job["metadata"]["name"]
|
|
|
|
if not isinstance(job_name, str):
|
|
|
|
raise exceptions.TutorError(
|
|
|
|
f"Invalid job name: '{job_name}'. Expected str."
|
|
|
|
)
|
|
|
|
if job_name == name:
|
|
|
|
return job
|
|
|
|
raise exceptions.TutorError(f"Could not find job '{name}'")
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
def active_job_names(self) -> List[str]:
|
|
|
|
"""
|
|
|
|
Return a list of active job names
|
|
|
|
Docs:
|
|
|
|
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#list-job-v1-batch
|
|
|
|
|
|
|
|
This is necessary to make sure that we don't run the same job multiple times at
|
|
|
|
the same time.
|
|
|
|
"""
|
|
|
|
api = K8sClients.instance().batch_api
|
|
|
|
return [
|
|
|
|
job.metadata.name
|
|
|
|
for job in api.list_namespaced_job(k8s_namespace(self.config)).items
|
|
|
|
if job.status.active
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class K8sContext(BaseTaskContext):
|
|
|
|
def job_runner(self, config: Config) -> K8sTaskRunner:
|
|
|
|
return K8sTaskRunner(self.root, config)
|
2022-04-12 12:49:01 +00:00
|
|
|
|
|
|
|
|
2019-10-08 20:25:31 +00:00
|
|
|
@click.group(help="Run Open edX on Kubernetes")
|
2022-04-12 12:49:01 +00:00
|
|
|
@click.pass_context
|
|
|
|
def k8s(context: click.Context) -> None:
|
|
|
|
context.obj = K8sContext(context.obj.root)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2019-05-05 09:45:24 +00:00
|
|
|
@click.command(help="Configure and run Open edX from scratch")
|
2019-06-05 17:45:22 +00:00
|
|
|
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
|
2021-02-25 08:09:14 +00:00
|
|
|
@click.pass_context
|
2022-09-30 10:00:24 +00:00
|
|
|
def launch(context: click.Context, non_interactive: bool) -> None:
|
2022-01-08 10:24:50 +00:00
|
|
|
run_upgrade_from_release = tutor_env.should_upgrade_from_release(context.obj.root)
|
|
|
|
if run_upgrade_from_release is not None:
|
2022-01-03 07:17:42 +00:00
|
|
|
click.echo(fmt.title("Upgrading from an older release"))
|
|
|
|
context.invoke(
|
|
|
|
upgrade,
|
2022-03-10 16:38:27 +00:00
|
|
|
from_release=tutor_env.get_env_release(context.obj.root),
|
2022-01-03 07:17:42 +00:00
|
|
|
)
|
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
click.echo(fmt.title("Interactive platform configuration"))
|
2022-04-18 22:33:55 +00:00
|
|
|
config = tutor_config.load_minimal(context.obj.root)
|
|
|
|
if not non_interactive:
|
|
|
|
interactive_config.ask_questions(config, run_for_prod=True)
|
|
|
|
tutor_config.save_config_file(context.obj.root, config)
|
|
|
|
config = tutor_config.load_full(context.obj.root)
|
|
|
|
tutor_env.save(context.obj.root, config)
|
2022-01-08 10:24:50 +00:00
|
|
|
|
|
|
|
if run_upgrade_from_release and not non_interactive:
|
|
|
|
question = f"""Your platform is being upgraded from {run_upgrade_from_release.capitalize()}.
|
|
|
|
|
|
|
|
If you run custom Docker images, you must rebuild and push them to your private repository now by running the following
|
|
|
|
commands in a different shell:
|
|
|
|
|
|
|
|
tutor images build all # add your custom images here
|
|
|
|
tutor images push all
|
|
|
|
|
|
|
|
Press enter when you are ready to continue"""
|
|
|
|
click.confirm(
|
|
|
|
fmt.question(question), default=True, abort=True, prompt_suffix=" "
|
2019-06-06 19:58:21 +00:00
|
|
|
)
|
2022-01-08 10:24:50 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
click.echo(fmt.title("Starting the platform"))
|
2021-02-25 08:09:14 +00:00
|
|
|
context.invoke(start)
|
2022-01-08 10:24:50 +00:00
|
|
|
|
2019-05-09 07:51:06 +00:00
|
|
|
click.echo(fmt.title("Database creation and migrations"))
|
2021-02-25 08:09:14 +00:00
|
|
|
context.invoke(init, limit=None)
|
2022-01-08 10:24:50 +00:00
|
|
|
|
|
|
|
config = tutor_config.load(context.obj.root)
|
2020-09-17 10:53:14 +00:00
|
|
|
fmt.echo_info(
|
|
|
|
"""Your Open edX platform is ready and can be accessed at the following urls:
|
|
|
|
|
|
|
|
{http}://{lms_host}
|
|
|
|
{http}://{cms_host}
|
|
|
|
""".format(
|
|
|
|
http="https" if config["ENABLE_HTTPS"] else "http",
|
|
|
|
lms_host=config["LMS_HOST"],
|
|
|
|
cms_host=config["CMS_HOST"],
|
|
|
|
)
|
|
|
|
)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2021-11-29 20:55:13 +00:00
|
|
|
@click.command(
|
|
|
|
short_help="Run all configured Open edX resources",
|
|
|
|
help=(
|
|
|
|
"Run all configured Open edX resources. You may limit this command to "
|
|
|
|
"some resources by passing name arguments."
|
|
|
|
),
|
|
|
|
)
|
|
|
|
@click.argument("names", metavar="name", nargs=-1)
|
2019-12-12 16:05:56 +00:00
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def start(context: K8sContext, names: List[str]) -> None:
|
2021-06-03 16:12:52 +00:00
|
|
|
config = tutor_config.load(context.root)
|
|
|
|
# Create namespace, if necessary
|
|
|
|
# Note that this step should not be run for some users, in particular those
|
|
|
|
# who do not have permission to edit the namespace.
|
|
|
|
try:
|
|
|
|
utils.kubectl("get", "namespaces", k8s_namespace(config))
|
|
|
|
fmt.echo_info("Namespace already exists: skipping creation.")
|
|
|
|
except exceptions.TutorError:
|
|
|
|
fmt.echo_info("Namespace does not exist: now creating it...")
|
2022-02-21 10:53:17 +00:00
|
|
|
kubectl_apply(
|
2022-03-24 07:17:23 +00:00
|
|
|
context.root,
|
2021-06-03 16:12:52 +00:00
|
|
|
"--wait",
|
|
|
|
"--selector",
|
|
|
|
"app.kubernetes.io/component=namespace",
|
|
|
|
)
|
2021-11-29 20:55:13 +00:00
|
|
|
|
|
|
|
names = names or ["all"]
|
|
|
|
for name in names:
|
|
|
|
if name == "all":
|
|
|
|
# Create volumes
|
2022-03-17 09:34:17 +00:00
|
|
|
kubectl_apply(
|
|
|
|
context.root,
|
2021-11-29 20:55:13 +00:00
|
|
|
"--wait",
|
|
|
|
"--selector",
|
|
|
|
"app.kubernetes.io/component=volume",
|
|
|
|
)
|
|
|
|
# Create everything else except jobs
|
2022-03-17 09:34:17 +00:00
|
|
|
kubectl_apply(
|
|
|
|
context.root,
|
2021-11-29 20:55:13 +00:00
|
|
|
"--selector",
|
|
|
|
"app.kubernetes.io/component notin (job,volume,namespace)",
|
|
|
|
)
|
|
|
|
else:
|
2022-03-17 09:34:17 +00:00
|
|
|
kubectl_apply(
|
|
|
|
context.root,
|
2021-11-29 20:55:13 +00:00
|
|
|
"--selector",
|
2022-01-08 10:24:50 +00:00
|
|
|
f"app.kubernetes.io/name={name}",
|
2021-11-29 20:55:13 +00:00
|
|
|
)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2021-11-29 20:55:13 +00:00
|
|
|
@click.command(
|
|
|
|
short_help="Stop a running platform",
|
|
|
|
help=(
|
|
|
|
"Stop a running platform by deleting all resources, except for volumes. "
|
|
|
|
"You may limit this command to some resources by passing name arguments."
|
|
|
|
),
|
|
|
|
)
|
|
|
|
@click.argument("names", metavar="name", nargs=-1)
|
2019-12-12 16:05:56 +00:00
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def stop(context: K8sContext, names: List[str]) -> None:
|
2019-12-12 16:05:56 +00:00
|
|
|
config = tutor_config.load(context.root)
|
2021-11-29 20:55:13 +00:00
|
|
|
names = names or ["all"]
|
|
|
|
for name in names:
|
|
|
|
if name == "all":
|
2022-01-03 07:17:42 +00:00
|
|
|
delete_resources(config)
|
2021-11-29 20:55:13 +00:00
|
|
|
else:
|
2022-01-03 07:17:42 +00:00
|
|
|
delete_resources(config, name=name)
|
|
|
|
|
|
|
|
|
|
|
|
def delete_resources(
|
|
|
|
config: Config, resources: Optional[List[str]] = None, name: Optional[str] = None
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
Delete resources by type and name.
|
|
|
|
|
|
|
|
The load balancer is never deleted.
|
|
|
|
"""
|
|
|
|
resources = resources or ["deployments", "services", "configmaps", "jobs"]
|
|
|
|
not_lb_selector = "app.kubernetes.io/component!=loadbalancer"
|
|
|
|
name_selector = [f"app.kubernetes.io/name={name}"] if name else []
|
|
|
|
utils.kubectl(
|
|
|
|
"delete",
|
|
|
|
*resource_selector(config, not_lb_selector, *name_selector),
|
|
|
|
",".join(resources),
|
|
|
|
)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2019-07-08 05:59:14 +00:00
|
|
|
@click.command(help="Reboot an existing platform")
|
2021-02-25 08:09:14 +00:00
|
|
|
@click.pass_context
|
|
|
|
def reboot(context: click.Context) -> None:
|
|
|
|
context.invoke(stop)
|
|
|
|
context.invoke(start)
|
2019-07-08 05:59:14 +00:00
|
|
|
|
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
@click.command(help="Completely delete an existing platform")
|
|
|
|
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
|
2019-12-12 16:05:56 +00:00
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def delete(context: K8sContext, yes: bool) -> None:
|
2019-01-22 20:25:04 +00:00
|
|
|
if not yes:
|
2019-05-05 09:45:24 +00:00
|
|
|
click.confirm(
|
|
|
|
"Are you sure you want to delete the platform? All data will be removed.",
|
|
|
|
abort=True,
|
|
|
|
)
|
2019-05-09 07:51:06 +00:00
|
|
|
utils.kubectl(
|
2019-12-12 16:05:56 +00:00
|
|
|
"delete",
|
|
|
|
"-k",
|
|
|
|
tutor_env.pathjoin(context.root),
|
|
|
|
"--ignore-not-found=true",
|
|
|
|
"--wait",
|
2019-05-09 07:51:06 +00:00
|
|
|
)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
@jobs.do_group
|
|
|
|
@click.pass_obj
|
|
|
|
def do(context: K8sContext) -> None:
|
|
|
|
"""
|
|
|
|
Run a custom job in the right container(s).
|
|
|
|
|
|
|
|
We make sure that some essential containers (databases, proxy) are up before we
|
|
|
|
launch the jobs.
|
|
|
|
"""
|
2022-11-11 13:13:36 +00:00
|
|
|
|
2022-10-19 15:46:31 +00:00
|
|
|
@hooks.Actions.DO_JOB.add()
|
|
|
|
def _start_base_deployments(_job_name: str, *_args: Any, **_kwargs: Any) -> None:
|
|
|
|
"""
|
|
|
|
We add this logic to an action callback because we do not want to trigger it
|
|
|
|
whenever we run `tutor k8s do <job> --help`.
|
|
|
|
"""
|
|
|
|
config = tutor_config.load(context.root)
|
|
|
|
wait_for_deployment_ready(config, "caddy")
|
|
|
|
for name in ["elasticsearch", "mysql", "mongodb"]:
|
|
|
|
if tutor_config.is_service_activated(config, name):
|
|
|
|
wait_for_deployment_ready(config, name)
|
|
|
|
|
|
|
|
|
2019-06-05 17:28:06 +00:00
|
|
|
@click.command(help="Initialise all applications")
|
2020-06-01 20:38:04 +00:00
|
|
|
@click.option("-l", "--limit", help="Limit initialisation to this service or plugin")
|
2022-10-19 15:46:31 +00:00
|
|
|
@click.pass_context
|
|
|
|
def init(context: click.Context, limit: Optional[str]) -> None:
|
|
|
|
context.invoke(do.commands["init"], limit=limit)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2021-11-30 17:02:14 +00:00
|
|
|
@click.command(help="Scale the number of replicas of a given deployment")
|
|
|
|
@click.argument("deployment")
|
|
|
|
@click.argument("replicas", type=int)
|
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def scale(context: K8sContext, deployment: str, replicas: int) -> None:
|
2021-11-30 17:02:14 +00:00
|
|
|
config = tutor_config.load(context.root)
|
|
|
|
utils.kubectl(
|
|
|
|
"scale",
|
|
|
|
# Note that we don't use the full resource selector because selectors
|
|
|
|
# are not compatible with the deployment/<name> argument.
|
|
|
|
*resource_namespace_selector(
|
|
|
|
config,
|
|
|
|
),
|
2022-01-08 10:24:50 +00:00
|
|
|
f"--replicas={replicas}",
|
|
|
|
f"deployment/{deployment}",
|
2021-11-30 17:02:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-04-12 12:49:01 +00:00
|
|
|
@click.command(
|
|
|
|
name="exec",
|
|
|
|
help="Execute a command in a pod of the given application",
|
|
|
|
context_settings={"ignore_unknown_options": True},
|
|
|
|
)
|
2019-06-06 19:58:21 +00:00
|
|
|
@click.argument("service")
|
2022-04-12 12:49:01 +00:00
|
|
|
@click.argument("args", nargs=-1, required=True)
|
2019-12-12 16:05:56 +00:00
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def exec_command(context: K8sContext, service: str, args: List[str]) -> None:
|
2019-12-12 16:05:56 +00:00
|
|
|
config = tutor_config.load(context.root)
|
2022-04-12 12:49:01 +00:00
|
|
|
kubectl_exec(config, service, args)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2019-05-09 07:51:06 +00:00
|
|
|
@click.command(help="View output from containers")
|
2019-06-06 19:58:21 +00:00
|
|
|
@click.option("-c", "--container", help="Print the logs of this specific container")
|
2019-05-09 07:51:06 +00:00
|
|
|
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
|
|
|
|
@click.option("--tail", type=int, help="Number of lines to show from each container")
|
|
|
|
@click.argument("service")
|
2019-12-12 16:05:56 +00:00
|
|
|
@click.pass_obj
|
2021-02-25 08:09:14 +00:00
|
|
|
def logs(
|
2022-04-12 12:49:01 +00:00
|
|
|
context: K8sContext, container: str, follow: bool, tail: bool, service: str
|
2021-02-25 08:09:14 +00:00
|
|
|
) -> None:
|
2019-12-12 16:05:56 +00:00
|
|
|
config = tutor_config.load(context.root)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-05-09 07:51:06 +00:00
|
|
|
command = ["logs"]
|
2019-06-05 19:01:02 +00:00
|
|
|
selectors = ["app.kubernetes.io/name=" + service] if service else []
|
|
|
|
command += resource_selector(config, *selectors)
|
2019-05-09 07:51:06 +00:00
|
|
|
|
2019-06-06 19:58:21 +00:00
|
|
|
if container:
|
|
|
|
command += ["-c", container]
|
2019-05-09 07:51:06 +00:00
|
|
|
if follow:
|
|
|
|
command += ["--follow"]
|
|
|
|
if tail is not None:
|
|
|
|
command += ["--tail", str(tail)]
|
|
|
|
|
|
|
|
utils.kubectl(*command)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2020-09-17 10:53:14 +00:00
|
|
|
@click.command(help="Wait for a pod to become ready")
|
|
|
|
@click.argument("name")
|
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def wait(context: K8sContext, name: str) -> None:
|
2020-09-17 10:53:14 +00:00
|
|
|
config = tutor_config.load(context.root)
|
2022-07-21 13:15:08 +00:00
|
|
|
wait_for_deployment_ready(config, name)
|
2020-09-17 10:53:14 +00:00
|
|
|
|
|
|
|
|
2022-01-08 10:24:50 +00:00
|
|
|
@click.command(
|
|
|
|
short_help="Perform release-specific upgrade tasks",
|
2022-09-30 10:00:24 +00:00
|
|
|
help="Perform release-specific upgrade tasks. To perform a full upgrade remember to run `launch`.",
|
2022-01-08 10:24:50 +00:00
|
|
|
)
|
2019-12-24 16:22:12 +00:00
|
|
|
@click.option(
|
2021-04-13 20:14:43 +00:00
|
|
|
"--from",
|
2022-01-08 10:24:50 +00:00
|
|
|
"from_release",
|
2022-12-12 22:28:22 +00:00
|
|
|
type=click.Choice(OPENEDX_RELEASE_NAMES),
|
2019-12-24 16:22:12 +00:00
|
|
|
)
|
2022-01-08 10:24:50 +00:00
|
|
|
@click.pass_context
|
|
|
|
def upgrade(context: click.Context, from_release: Optional[str]) -> None:
|
|
|
|
if from_release is None:
|
|
|
|
from_release = tutor_env.get_env_release(context.obj.root)
|
|
|
|
if from_release is None:
|
|
|
|
fmt.echo_info("Your environment is already up-to-date")
|
|
|
|
else:
|
|
|
|
fmt.echo_alert(
|
|
|
|
"This command only performs a partial upgrade of your Open edX platform. "
|
2022-09-30 10:00:24 +00:00
|
|
|
"To perform a full upgrade, you should run `tutor k8s launch`."
|
2022-01-08 10:24:50 +00:00
|
|
|
)
|
2022-12-13 15:39:35 +00:00
|
|
|
upgrade_from(context, from_release)
|
2022-01-08 10:24:50 +00:00
|
|
|
# We update the environment to update the version
|
|
|
|
context.invoke(config_save_command)
|
2022-01-03 07:17:42 +00:00
|
|
|
|
|
|
|
|
2022-02-21 10:53:17 +00:00
|
|
|
@click.command(
|
|
|
|
short_help="Direct interface to `kubectl apply`.",
|
|
|
|
help=(
|
|
|
|
"Direct interface to `kubnectl-apply`. This is a wrapper around `kubectl apply`. A;; options and"
|
|
|
|
" arguments passed to this command will be forwarded as-is to `kubectl apply`."
|
|
|
|
),
|
|
|
|
context_settings={"ignore_unknown_options": True},
|
|
|
|
name="apply",
|
|
|
|
)
|
|
|
|
@click.argument("args", nargs=-1)
|
|
|
|
@click.pass_obj
|
2022-04-12 12:49:01 +00:00
|
|
|
def apply_command(context: K8sContext, args: List[str]) -> None:
|
2022-02-21 10:53:17 +00:00
|
|
|
kubectl_apply(context.root, *args)
|
|
|
|
|
|
|
|
|
|
|
|
def kubectl_apply(root: str, *args: str) -> None:
|
|
|
|
utils.kubectl("apply", "--kustomize", tutor_env.pathjoin(root), *args)
|
|
|
|
|
|
|
|
|
2022-04-08 16:02:21 +00:00
|
|
|
@click.command(help="Print status information for all k8s resources")
|
|
|
|
@click.pass_obj
|
|
|
|
def status(context: K8sContext) -> int:
|
|
|
|
config = tutor_config.load(context.root)
|
|
|
|
return utils.kubectl("get", "all", *resource_namespace_selector(config))
|
|
|
|
|
|
|
|
|
2022-04-12 12:49:01 +00:00
|
|
|
def kubectl_exec(config: Config, service: str, command: List[str]) -> int:
|
2022-01-08 10:24:50 +00:00
|
|
|
selector = f"app.kubernetes.io/name={service}"
|
Improve job running in local and k8s
Running jobs was previously done with "exec". This was because it
allowed us to avoid copying too much container specification information
from the docker-compose/deployments files to the jobs files. However,
this was limiting:
- In order to run a job, the corresponding container had to be running.
This was particularly painful in Kubernetes, where containers are
crashing as long as migrations are not correctly run.
- Containers in which we need to run jobs needed to be present in the
docker-compose/deployments files. This is unnecessary, for example when
mysql is disabled, or in the case of the certbot container.
Now, we create dedicated jobs files, both for local and k8s deployment.
This introduces a little redundancy, but not too much. Note that
dependent containers are not listed in the docker-compose.jobs.yml file,
so an actual platform is still supposed to be running when we launch the
jobs.
This also introduces a subtle change: now, jobs go through the container
entrypoint prior to running. This is probably a good thing, as it will
avoid forgetting about incorrect environment variables.
In k8s, we find ourselves interacting way too much with the kubectl
utility. Parsing output from the CLI is a pain. So we need to switch to
the native kubernetes client library.
2020-03-25 17:47:36 +00:00
|
|
|
pods = K8sClients.instance().core_api.list_namespaced_pod(
|
2021-06-03 16:12:52 +00:00
|
|
|
namespace=k8s_namespace(config), label_selector=selector
|
2019-06-06 19:58:21 +00:00
|
|
|
)
|
Improve job running in local and k8s
Running jobs was previously done with "exec". This was because it
allowed us to avoid copying too much container specification information
from the docker-compose/deployments files to the jobs files. However,
this was limiting:
- In order to run a job, the corresponding container had to be running.
This was particularly painful in Kubernetes, where containers are
crashing as long as migrations are not correctly run.
- Containers in which we need to run jobs needed to be present in the
docker-compose/deployments files. This is unnecessary, for example when
mysql is disabled, or in the case of the certbot container.
Now, we create dedicated jobs files, both for local and k8s deployment.
This introduces a little redundancy, but not too much. Note that
dependent containers are not listed in the docker-compose.jobs.yml file,
so an actual platform is still supposed to be running when we launch the
jobs.
This also introduces a subtle change: now, jobs go through the container
entrypoint prior to running. This is probably a good thing, as it will
avoid forgetting about incorrect environment variables.
In k8s, we find ourselves interacting way too much with the kubectl
utility. Parsing output from the CLI is a pain. So we need to switch to
the native kubernetes client library.
2020-03-25 17:47:36 +00:00
|
|
|
if not pods.items:
|
|
|
|
raise exceptions.TutorError(
|
2022-01-08 10:24:50 +00:00
|
|
|
f"Could not find an active pod for the {service} service"
|
Improve job running in local and k8s
Running jobs was previously done with "exec". This was because it
allowed us to avoid copying too much container specification information
from the docker-compose/deployments files to the jobs files. However,
this was limiting:
- In order to run a job, the corresponding container had to be running.
This was particularly painful in Kubernetes, where containers are
crashing as long as migrations are not correctly run.
- Containers in which we need to run jobs needed to be present in the
docker-compose/deployments files. This is unnecessary, for example when
mysql is disabled, or in the case of the certbot container.
Now, we create dedicated jobs files, both for local and k8s deployment.
This introduces a little redundancy, but not too much. Note that
dependent containers are not listed in the docker-compose.jobs.yml file,
so an actual platform is still supposed to be running when we launch the
jobs.
This also introduces a subtle change: now, jobs go through the container
entrypoint prior to running. This is probably a good thing, as it will
avoid forgetting about incorrect environment variables.
In k8s, we find ourselves interacting way too much with the kubectl
utility. Parsing output from the CLI is a pain. So we need to switch to
the native kubernetes client library.
2020-03-25 17:47:36 +00:00
|
|
|
)
|
|
|
|
pod_name = pods.items[0].metadata.name
|
2019-06-06 19:58:21 +00:00
|
|
|
|
|
|
|
# Run command
|
2021-02-25 08:09:14 +00:00
|
|
|
return utils.kubectl(
|
2019-06-06 19:58:21 +00:00
|
|
|
"exec",
|
2022-04-12 12:49:01 +00:00
|
|
|
"--stdin",
|
|
|
|
"--tty",
|
2019-06-06 19:58:21 +00:00
|
|
|
"--namespace",
|
2021-04-06 10:09:00 +00:00
|
|
|
k8s_namespace(config),
|
Improve job running in local and k8s
Running jobs was previously done with "exec". This was because it
allowed us to avoid copying too much container specification information
from the docker-compose/deployments files to the jobs files. However,
this was limiting:
- In order to run a job, the corresponding container had to be running.
This was particularly painful in Kubernetes, where containers are
crashing as long as migrations are not correctly run.
- Containers in which we need to run jobs needed to be present in the
docker-compose/deployments files. This is unnecessary, for example when
mysql is disabled, or in the case of the certbot container.
Now, we create dedicated jobs files, both for local and k8s deployment.
This introduces a little redundancy, but not too much. Note that
dependent containers are not listed in the docker-compose.jobs.yml file,
so an actual platform is still supposed to be running when we launch the
jobs.
This also introduces a subtle change: now, jobs go through the container
entrypoint prior to running. This is probably a good thing, as it will
avoid forgetting about incorrect environment variables.
In k8s, we find ourselves interacting way too much with the kubectl
utility. Parsing output from the CLI is a pain. So we need to switch to
the native kubernetes client library.
2020-03-25 17:47:36 +00:00
|
|
|
pod_name,
|
2019-06-06 19:58:21 +00:00
|
|
|
"--",
|
2022-04-12 12:49:01 +00:00
|
|
|
*command,
|
2019-06-06 19:58:21 +00:00
|
|
|
)
|
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2022-07-21 13:15:08 +00:00
|
|
|
def wait_for_deployment_ready(config: Config, service: str) -> None:
|
|
|
|
fmt.echo_info(f"Waiting for a {service} deployment to be ready...")
|
2019-06-05 19:01:02 +00:00
|
|
|
utils.kubectl(
|
|
|
|
"wait",
|
2022-01-08 10:24:50 +00:00
|
|
|
*resource_selector(config, f"app.kubernetes.io/name={service}"),
|
2022-07-21 13:15:08 +00:00
|
|
|
"--for=condition=Available=True",
|
2019-06-05 19:01:02 +00:00
|
|
|
"--timeout=600s",
|
2022-07-21 13:15:08 +00:00
|
|
|
"deployment",
|
2019-06-05 19:01:02 +00:00
|
|
|
)
|
|
|
|
|
2019-04-23 07:57:55 +00:00
|
|
|
|
2021-11-30 17:02:14 +00:00
|
|
|
def resource_selector(config: Config, *selectors: str) -> List[str]:
|
|
|
|
"""
|
|
|
|
Convenient utility to filter the resources that belong to this project.
|
|
|
|
"""
|
|
|
|
selector = ",".join(
|
|
|
|
["app.kubernetes.io/instance=openedx-" + get_typed(config, "ID", str)]
|
|
|
|
+ list(selectors)
|
|
|
|
)
|
|
|
|
return resource_namespace_selector(config) + ["--selector=" + selector]
|
|
|
|
|
|
|
|
|
|
|
|
def resource_namespace_selector(config: Config) -> List[str]:
|
|
|
|
"""
|
|
|
|
Convenient utility to filter the resources that belong to this project namespace.
|
|
|
|
"""
|
|
|
|
return ["--namespace", k8s_namespace(config)]
|
|
|
|
|
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def k8s_namespace(config: Config) -> str:
|
|
|
|
return get_typed(config, "K8S_NAMESPACE", str)
|
|
|
|
|
|
|
|
|
2022-09-30 10:00:24 +00:00
|
|
|
k8s.add_command(launch)
|
2019-01-22 20:25:04 +00:00
|
|
|
k8s.add_command(start)
|
|
|
|
k8s.add_command(stop)
|
2019-07-08 05:59:14 +00:00
|
|
|
k8s.add_command(reboot)
|
2019-01-22 20:25:04 +00:00
|
|
|
k8s.add_command(delete)
|
2019-06-05 17:28:06 +00:00
|
|
|
k8s.add_command(init)
|
2021-11-30 17:02:14 +00:00
|
|
|
k8s.add_command(scale)
|
2019-06-06 19:58:21 +00:00
|
|
|
k8s.add_command(exec_command)
|
2019-05-09 07:51:06 +00:00
|
|
|
k8s.add_command(logs)
|
2020-09-17 10:53:14 +00:00
|
|
|
k8s.add_command(wait)
|
2019-12-24 16:22:12 +00:00
|
|
|
k8s.add_command(upgrade)
|
2022-02-21 10:53:17 +00:00
|
|
|
k8s.add_command(apply_command)
|
2022-04-08 16:02:21 +00:00
|
|
|
k8s.add_command(status)
|
2022-10-19 15:46:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
@hooks.Actions.PLUGINS_LOADED.add()
|
|
|
|
def _add_k8s_do_commands() -> None:
|
|
|
|
jobs.add_job_commands(do)
|
|
|
|
k8s.add_command(do)
|