From f48a8c89d180d8399ecccb9dd31c5bd158d5db93 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Thu, 23 Jan 2020 05:28:33 -0800 Subject: [PATCH] Delete the old jupyter release infrastructure * The infrastructure for continuously rebuilding our docker images and updating our kustomize manifests has now been generalized. see kubeflow/testing#450 and https://github.com/kubeflow/testing/tree/master/apps-cd * This is the old code for updating the jupyter web app and is no longer needed. --- .../kubeflow/ci/update_jupyter_web_app.py | 292 ------------------ releasing/auto-update/README.md | 94 ------ releasing/auto-update/base/kustomization.yaml | 9 - releasing/auto-update/base/params.yaml | 9 - releasing/auto-update/base/scripts.yaml | 16 - .../overlays/cron/kustomization.yaml | 9 - .../overlays/cron/update_job_cron.yaml | 107 ------- .../overlays/job/kustomization.yaml | 9 - .../auto-update/overlays/job/update_job.yaml | 105 ------- 9 files changed, 650 deletions(-) delete mode 100644 py/kubeflow/kubeflow/ci/update_jupyter_web_app.py delete mode 100644 releasing/auto-update/README.md delete mode 100644 releasing/auto-update/base/kustomization.yaml delete mode 100644 releasing/auto-update/base/params.yaml delete mode 100644 releasing/auto-update/base/scripts.yaml delete mode 100644 releasing/auto-update/overlays/cron/kustomization.yaml delete mode 100644 releasing/auto-update/overlays/cron/update_job_cron.yaml delete mode 100644 releasing/auto-update/overlays/job/kustomization.yaml delete mode 100644 releasing/auto-update/overlays/job/update_job.yaml diff --git a/py/kubeflow/kubeflow/ci/update_jupyter_web_app.py b/py/kubeflow/kubeflow/ci/update_jupyter_web_app.py deleted file mode 100644 index 1173df0ae9d..00000000000 --- a/py/kubeflow/kubeflow/ci/update_jupyter_web_app.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Script to build and update the Jupyter WebApp image. - -Requires python3 - -hub CLI depends on an OAuth token with repo permissions: -https://hub.github.com/hub.1.html - * It will look for environment variable GITHUB_TOKEN -""" - -import logging -import os -import tempfile -import yaml - -import fire -import git -import httplib2 - -from kubeflow.kubeflow.ci import application_util -from kubeflow.testing import util # pylint: disable=no-name-in-module - -from containerregistry.client import docker_creds -from containerregistry.client import docker_name -from containerregistry.client.v2_2 import docker_http -from containerregistry.client.v2_2 import docker_image as v2_2_image -from containerregistry.transport import transport_pool - -# The image name as defined in the kustomization file -JUPYTER_WEB_APP_IMAGE_NAME = "gcr.io/kubeflow-images-public/jupyter-web-app" - -class WebAppUpdater(object): # pylint: disable=useless-object-inheritance - def __init__(self): - self._last_commit = None - self.manifests_repo_dir = None - - def build_image(self, build_project, registry_project): - """Build the image. - - Args: - build_project: GCP project used to build the image. - registry_project: GCP project used to host the image. - """ - env = dict() - env.update(os.environ) - env["PROJECT"] = build_project - env["REGISTRY_PROJECT"] = registry_project - env["GIT_TAG"] = self._last_commit - - with tempfile.NamedTemporaryFile() as hf: - name = hf.name - env["OUTPUT"] = name - web_dir = self._component_dir() - util.run(["make", "build-gcb"], env=env, cwd=web_dir) - - # TODO(jlewi): We want to get the actual image produced by GCB. Right - # now this is a bit brittle because we have multiple layers of substitution - # e.g. in the Makefile and then the GCB YAML. - # It might be better to parse the stdout of make-build-gcb to get the - # GCB job name and then fetch the GCB info specifying the images. - with open(name) as hf: - data = yaml.load(hf) - - return data["image"] - - @property - def last_commit(self): - """Get the last commit of a change to the source for the jupyter-web-app.""" - if not self._last_commit: - # Get the hash of the last commit to modify the source for the Jupyter web - # app image - self._last_commit = util.run(["git", "log", "-n", "1", - "--pretty=format:\"%h\"", - "components/jupyter-web-app"], - cwd=self._root_dir()).strip("\"") - - return self._last_commit - - def _find_remote_repo(self, repo, remote_url): # pylint: disable=no-self-use - """Find the remote repo if it has already been added. - - Args: - repo: The git python repo object. - remote_url: The URL of the remote repo e.g. - git@github.com:jlewi/kubeflow.git - - Returns: - remote: git-python object representing the remote repo or none if it - isn't present. - """ - for r in repo.remotes: - for u in r.urls: - if remote_url == u: - return r - - return None - - def all(self, build_project, registry_project, remote_fork, # pylint: disable=too-many-statements,too-many-branches - kustomize_file, add_github_host=False): - """Build the latest image and update the prototype. - - Args: - build_project: GCP project used to build the image. - registry_project: GCP project used to host the image. - remote_fork: Url of the remote fork. - The remote fork used to create the PR; - e.g. git@github.com:jlewi/kubeflow.git. currently only ssh is - supported. - kustomize_file: Path to the kustomize file - add_github_host: If true will add the github ssh host to known ssh hosts. - """ - # TODO(jlewi): How can we automatically determine the root of the git - # repo containing the kustomize_file? - self.manifests_repo_dir = util.run(["git", "rev-parse", "--show-toplevel"], - cwd=os.path.dirname(kustomize_file)) - repo = git.Repo(self.manifests_repo_dir) - util.maybe_activate_service_account() - last_commit = self.last_commit - - # Ensure github.com is in the known hosts - if add_github_host: - output = util.run(["ssh-keyscan", "github.com"]) - with open(os.path.join(os.getenv("HOME"), ".ssh", "known_hosts"), - mode='a') as hf: - hf.write(output) - - if not remote_fork.startswith("git@github.com"): - raise ValueError("Remote fork currently only supports ssh") - - remote_repo = self._find_remote_repo(repo, remote_fork) - - if not remote_repo: - fork_name = remote_fork.split(":", 1)[-1].split("/", 1)[0] - logging.info("Adding remote %s=%s", fork_name, remote_fork) - remote_repo = repo.create_remote(fork_name, remote_fork) - - logging.info("Last change to components-jupyter-web-app was %s", last_commit) - - base = "gcr.io/{0}/jupyter-web-app".format(registry_project) - - # Check if there is already an image tagged with this commit. - image = base + ":" + self.last_commit - transport = transport_pool.Http(httplib2.Http) - src = docker_name.from_string(image) - creds = docker_creds.DefaultKeychain.Resolve(src) - - image_exists = False - try: - with v2_2_image.FromRegistry(src, creds, transport) as src_image: - logging.info("Image %s exists; digest: %s", image, - src_image.digest()) - image_exists = True - except docker_http.V2DiagnosticException as e: - if e.status == 404: - logging.info("%s doesn't exist", image) - else: - raise - - if not image_exists: - logging.info("Building the image") - image = self.build_image(build_project, registry_project) - logging.info("Created image: %s", image) - else: - logging.info("Image %s already exists", image) - - # TODO(jlewi): What if the file was already modified so we didn't - # modify it in this run but we still need to commit it? - image_updated = application_util.set_kustomize_image( - kustomize_file, JUPYTER_WEB_APP_IMAGE_NAME, image) - - if not image_updated: - logging.info("kustomization not updated so not creating a PR.") - return - - application_util.regenerate_manifest_tests(self.manifests_repo_dir) - - branch_name = "update_jupyter_{0}".format(last_commit) - - if repo.active_branch.name != branch_name: - logging.info("Creating branch %s", branch_name) - - branch_names = [b.name for b in repo.branches] - if branch_name in branch_names: - logging.info("Branch %s exists", branch_name) - util.run(["git", "checkout", branch_name], cwd=self.manifests_repo_dir) - else: - util.run(["git", "checkout", "-b", branch_name], - cwd=self.manifests_repo_dir) - - if self._check_if_pr_exists(commit=last_commit): - # Since a PR already exists updating to the specified commit - # don't create a new one. - # We don't want to just push -f because if the PR already exists - # git push -f will retrigger the tests. - # To force a recreate of the PR someone could close the existing - # PR and a new PR will be created on the next cron run. - return - - logging.info("Add file %s to repo", kustomize_file) - repo.index.add([kustomize_file]) - repo.index.add([os.path.join(self.manifests_repo_dir, "tests/*")]) - repo.index.commit("Update the jupyter web app image to {0}".format(image)) - - util.run(["git", "push", "-f", remote_repo.name, - "{0}:{0}".format(branch_name)], - cwd=self.manifests_repo_dir) - - self.create_pull_request(commit=last_commit) - - def _pr_title(self, commit): - pr_title = "[auto PR] Update the jupyter-web-app image to {0}".format( - commit) - return pr_title - - def _check_if_pr_exists(self, commit=None): - """Check if a PR is already open. - - Returns: - exists: True if a PR updating the image to the specified commit already - exists and false otherwise. - """ - # TODO(jlewi): Modeled on - # https://github.com/kubeflow/examples/blob/master/code_search/docker/ks/update_index.sh - # TODO(jlewi): We should use the GitHub API and check if there is an - # existing open pull request. Or potentially just use the hub CLI. - - if not commit: - commit = self.last_commit - logging.info("No commit specified defaulting to %s", commit) - - pr_title = self._pr_title(commit) - - # See hub conventions: - # https://hub.github.com/hub.1.html - # The GitHub repository is determined automatically based on the name - # of remote repositories - output = util.run(["hub", "pr", "list", "--format=%U;%t\n"], - cwd=self.manifests_repo_dir) - - - lines = output.splitlines() - - prs = {} - for l in lines: - n, t = l.split(";", 1) - prs[t] = n - - if pr_title in prs: - logging.info("PR %s already exists to update the Jupyter web app image " - "to %s", prs[pr_title], commit) - return True - - return False - - def create_pull_request(self, base="kubeflow:master", commit=None): - """Create a pull request. - - Args: - base: The base to use. Defaults to "kubeflow:master". This should be - in the form : - """ - pr_title = self._pr_title(commit) - - with tempfile.NamedTemporaryFile(delete=False, mode="w") as hf: - hf.write(pr_title) - hf.write("\n") - hf.write("\n") - hf.write( - "Image built from commit https://github.com/kubeflow/kubeflow/" - "commit/{0}".format(self._last_commit)) - message_file = hf.name - - # TODO(jlewi): -f creates the pull requests even if there are local changes - # this was useful during development but we may want to drop it. - util.run(["hub", "pull-request", "-f", "--base=" + base, "-F", - message_file], - cwd=self.manifests_repo_dir) - - def _root_dir(self): - this_dir = os.path.dirname(__file__) - return os.path.abspath(os.path.join(this_dir, "..", "..", "..", "..")) - - def _component_dir(self): - return os.path.join(self._root_dir(), "components", "jupyter-web-app") - -if __name__ == '__main__': - logging.basicConfig(level=logging.INFO, - format=('%(levelname)s|%(asctime)s' - '|%(pathname)s|%(lineno)d| %(message)s'), - datefmt='%Y-%m-%dT%H:%M:%S', - ) - logging.getLogger().setLevel(logging.INFO) - fire.Fire(WebAppUpdater) diff --git a/releasing/auto-update/README.md b/releasing/auto-update/README.md deleted file mode 100644 index 85101fa3f0a..00000000000 --- a/releasing/auto-update/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Auto Update of Kubeflow Manifests - -This directory contains Kubernetes jobs to: - -* Continuous rebuild docker images for Kubeflow applications when the source code changes -* Update the Kubeflow manifests to use the new images -* Create PRs to update the manifests. - -We are currently prototyping and building this out using the Kubeflow Jupyter Web Application. - -Here's how this works - -* A python script py/kubeflow/kubeflow/ci/update_jupyter_web_app.py is used to build the image and create a PR - - * The script will only build a docker image if there has been a change to the jupyter web app source code since the last image - - * If a new image is built we update the Kubeflow ksonnet prototype to use the new image - - * The script uses the [hub cli](https://hub.github.com/) to create a PR - - -* update_job.yaml provides an example job spec for running the job. - - -## kubeflow-bot - -* The script uses the [kubeflow-bot](https://github.com/kubeflow-bot) to host the fork containing the PR -* The ssh key for the kubeflow-bot account is stored as a K8s secret - - * We use - - * **project**: kubeflow-releasing - * **cluster**: kf-releasing-v-0-6-2 - * **kf-releasing**: This is a Kubeflow profile created namespace - * This is a kubeflow v0.6.2 cluster; configs should be checked in kubeflow/testing/release-infra - -* The ssh keys are stored as K8s secret and configured via init containers - -## Setting up the bot - -1. Create secrets in the cluster containing the GitHub ssh keys. - - ``` - kubectl create -n ${NAMESPACE} secret generic kubeflow-bot-ssh --from-file=id_rsa=kubeflow-bot --from-file=id_rsa.pub=kubeflow-bot.pub - ``` - - * Use a public/private SSH key that has been added to the kubeflow-bot GitHub account - -1. Create a secret containing a GITHUB_TOKEN - - ``` - kubectl create secret generic github-token --namespace=${NAMESPACE} --from-literal=github_token=${GITHUB_TOKEN} - ``` - -## hub-cli - -[hub cli](https://hub.github.com/) is to list and create PRs - -Here are some key things to know about hub CLI - - * It uses a GITHUB_TOKEN to create PRs - - * The token must have permission to modify repositories in order to create PRs see https://hub.github.com/hub.1.html - - * The repository in which to create the PR is based on the names of the remotes see conventions in the [doc page](https://hub.github.com/hub.1.html) - - -## Kustomize package - -Common variables should be set in `base/kustomization.yaml` and `base/params.env`; e.g. - - * Base docker image - * Repos to check out - * Values for other flags - -There are two overlays corresponding to a cron job and a batch job which can be used for one off runs. - -To override the parameters you could add a section to the kustomization.yaml file overlay - -``` -configMapGenerator: -- name: params - behavior: merge - env: params.env -``` - -TODO(jlewi): It might be better to mount a config map with the startup scripts - -## Next steps - -* We'd like to use Kubeflow to run this script regularly and keep track of runs; options are - - * Run it as a 1 step pipeline and use pipelines to keep track of runs - * Use a cron job and the Kubeflow metadata solution to keep track of runs \ No newline at end of file diff --git a/releasing/auto-update/base/kustomization.yaml b/releasing/auto-update/base/kustomization.yaml deleted file mode 100644 index 79c4f1b7830..00000000000 --- a/releasing/auto-update/base/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- params.yaml -- scripts.yaml -namePrefix: auto-update- -namespace: kubeflow -commonLabels: - app: auto-updater diff --git a/releasing/auto-update/base/params.yaml b/releasing/auto-update/base/params.yaml deleted file mode 100644 index 606a23020a8..00000000000 --- a/releasing/auto-update/base/params.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# A config map used to configure various parameters used by the update jobs. -apiVersion: v1 -data: -kind: ConfigMap -metadata: - name: params -data: - # TODO(jlewi): change to kubeflow/kubeflow@HEAD when PR #3958 is merged - repos: "kubeflow/kubeflow@HEAD:4029,kubeflow/fairing@HEAD,kubeflow/testing@HEAD,kubeflow/manifests@HEAD" \ No newline at end of file diff --git a/releasing/auto-update/base/scripts.yaml b/releasing/auto-update/base/scripts.yaml deleted file mode 100644 index b296d38fd8e..00000000000 --- a/releasing/auto-update/base/scripts.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Use a configmap to define wrapper scripts to invoke each container -# this is a way to keep commands in sync in the job and cron job. -apiVersion: v1 -data: - update_jupyter.sh: | - #!/bin/bash - set -ex - python3 -m kubeflow.kubeflow.ci.update_jupyter_web_app all \ - --build-project=kubeflow-releasing \ - --registry-project=kubeflow-images-public \ - --remote-fork=git@github.com:kubeflow-bot/manifests.git \ - --kustomize_file=/src/kubeflow/manifests/jupyter/jupyter-web-app/base/kustomization.yaml \ - --add-github-host=true -kind: ConfigMap -metadata: - name: scripts \ No newline at end of file diff --git a/releasing/auto-update/overlays/cron/kustomization.yaml b/releasing/auto-update/overlays/cron/kustomization.yaml deleted file mode 100644 index 07c07b5974a..00000000000 --- a/releasing/auto-update/overlays/cron/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -namespace: kf-releasing -bases: - - ../../base -resources: - - update_job_cron.yaml -images: -- name: gcr.io/kubeflow-releasing/test-worker - newName: gcr.io/kubeflow-releasing/test-worker - newTag: v20190905-cff62af-dirty-f45ce7 diff --git a/releasing/auto-update/overlays/cron/update_job_cron.yaml b/releasing/auto-update/overlays/cron/update_job_cron.yaml deleted file mode 100644 index 422fc70e150..00000000000 --- a/releasing/auto-update/overlays/cron/update_job_cron.yaml +++ /dev/null @@ -1,107 +0,0 @@ -# Run a job to build the image from the latest code and then create -# a PR updating the image. -# -# This job is expected to run in the cluster kf-releasing in project kubeflow-releasing. -apiVersion: batch/v1beta1 -kind: CronJob -metadata: - labels: - app: jupyter-updater - name: jupyter-updater -spec: - # TODO(jlewi): Run this more frequently once we see we aren't spamming the repo with PRs - schedule: "*/6 * * * *" - jobTemplate: - spec: - template: - metadata: - annotations: - sidecar.istio.io/inject: "false" - labels: - app: jupyter-updater-cron - spec: - initContainers: - # This init container checks out the source code. - - command: - - /usr/local/bin/checkout_repos.sh - # TODO(jlewi): Stop checking out the jlewi fork once it is merged - - --repos=$(REPOS) - - --src_dir=/src - # Don't do a shallow clone because we need to compute when files were modified and shallow clones prevent pushing - # branches. - - --depth=all - name: checkout - image: gcr.io/kubeflow-releasing/test-worker - volumeMounts: - - mountPath: /src - name: src - env: - - name: REPOS - valueFrom: - configMapKeyRef: - # The name here includes the common prefix - # TODO(jlewi): Is it expected that the common prefix needs to be included here. - name: auto-update-params - key: repos - # This init container configures the ssh keys - - command: - - /usr/local/bin/setup_ssh.sh - - --ssh_dir=/root/.ssh - - --private_key=/secret/ssh-key/id_rsa - - --public_key=/secret/ssh-key/id_rsa.pub - image: gcr.io/kubeflow-releasing/test-worker - name: setup-ssh - volumeMounts: - - mountPath: /root/.ssh - name: ssh-config - - mountPath: /secret/ssh-key - name: ssh - readOnly: true - containers: - - command: - - bash - - /scripts/update_jupyter.sh - image: gcr.io/kubeflow-releasing/test-worker - name: update - env: - - name: GIT_AUTHOR_NAME - value: kubeflow-bot - - name: GIT_AUTHOR_EMAIL - value: ci-bot-owners@kubeflow.org - - name: GOOGLE_APPLICATION_CREDENTIALS - value: /secret/gcp-credentials/user-gcp-sa.json - - name: PYTHONPATH - value: /src/kubeflow/kubeflow/py:/src/kubeflow/testing/py:/src/kubeflow/fairing - - name: GITHUB_TOKEN - valueFrom: - secretKeyRef: - name: github-token - key: github_token - - name: GOPATH - value: /src/go - volumeMounts: - - mountPath: /src - name: src - - mountPath: /secret/gcp-credentials - name: gcp-credentials - readOnly: true - - mountPath: /root/.ssh - name: ssh-config - - mountPath: /scripts - name: scripts - restartPolicy: Never - volumes: - - name: ssh-config - emptyDir: {} - - name: src - emptyDir: {} - - name: gcp-credentials - secret: - secretName: user-gcp-sa - # ssh key to push to github.com/kubeflow-bot - - name: ssh - secret: - secretName: kubeflow-bot-ssh - - name: scripts - configMap: - name: auto-update-scripts \ No newline at end of file diff --git a/releasing/auto-update/overlays/job/kustomization.yaml b/releasing/auto-update/overlays/job/kustomization.yaml deleted file mode 100644 index b80e98e7613..00000000000 --- a/releasing/auto-update/overlays/job/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -namespace: kf-releasing -bases: - - ../../base -resources: - - update_job.yaml -images: -- name: gcr.io/kubeflow-releasing/test-worker - newName: gcr.io/kubeflow-releasing/test-worker - newTag: v20190905-cff62af-dirty-f45ce7 \ No newline at end of file diff --git a/releasing/auto-update/overlays/job/update_job.yaml b/releasing/auto-update/overlays/job/update_job.yaml deleted file mode 100644 index 6722bd003c2..00000000000 --- a/releasing/auto-update/overlays/job/update_job.yaml +++ /dev/null @@ -1,105 +0,0 @@ -# Run a job to build the image from the latest code and then create -# a PR updating the image. -# -# This job is expected to run in the cluster kf-releasing in project kubeflow-releasing. -apiVersion: batch/v1 -kind: Job -metadata: - labels: - app: jupyter-updater - # kustomize seems to chocke on generateName - # generateName: jupyter-updater- - name: jupyter -spec: - template: - metadata: - annotations: - sidecar.istio.io/inject: "false" - spec: - initContainers: - # This init container checks out the source code. - - command: - - /usr/local/bin/checkout_repos.sh - # TODO(jlewi): Why are we checkout out fairing? - # TODO(jlewi): Stop checking out PR for kubeflow/kubeflow once its merged - #- --repos=kubeflow/kubeflow@HEAD:3958,kubeflow/fairing@HEAD,kubeflow/testing@HEAD,kubeflow/manifests@HEAD - - --repos=$(REPOS) - - --src_dir=/src - # Don't do a shallow clone because we need to compute when files were modified and shallow clones prevent pushing - # branches. - - --depth=all - name: checkout - image: gcr.io/kubeflow-releasing/test-worker - volumeMounts: - - mountPath: /src - name: src - env: - - name: REPOS - valueFrom: - configMapKeyRef: - # The name here includes the common prefix - # TODO(jlewi): Is it expected that the common prefix needs to be included here. - name: auto-update-params - key: repos - # This init container configures the ssh keys - - command: - - /usr/local/bin/setup_ssh.sh - - --ssh_dir=/root/.ssh - - --private_key=/secret/ssh-key/id_rsa - - --public_key=/secret/ssh-key/id_rsa.pub - image: gcr.io/kubeflow-releasing/test-worker - name: setup-ssh - volumeMounts: - - mountPath: /root/.ssh - name: ssh-config - - mountPath: /secret/ssh-key - name: ssh - readOnly: true - containers: - - command: - - bash - - /scripts/update_jupyter.sh - image: gcr.io/kubeflow-releasing/test-worker - name: update - env: - - name: GIT_AUTHOR_NAME - value: kubeflow-bot - - name: GIT_AUTHOR_EMAIL - value: ci-bot-owners@kubeflow.org - - name: GOOGLE_APPLICATION_CREDENTIALS - value: /secret/gcp-credentials/user-gcp-sa.json - - name: PYTHONPATH - value: /src/kubeflow/kubeflow/py:/src/kubeflow/testing/py:/src/kubeflow/fairing - - name: GITHUB_TOKEN - valueFrom: - secretKeyRef: - name: github-token - key: github_token - - name: GOPATH - value: /src/go - volumeMounts: - - mountPath: /src - name: src - - mountPath: /secret/gcp-credentials - name: gcp-credentials - readOnly: true - - mountPath: /root/.ssh - name: ssh-config - - mountPath: /scripts - name: scripts - restartPolicy: Never - volumes: - - name: ssh-config - emptyDir: {} - - name: src - emptyDir: {} - - name: gcp-credentials - secret: - secretName: user-gcp-sa - # ssh key to push to github.com/kubeflow-bot - - name: ssh - secret: - secretName: kubeflow-bot-ssh - - name: scripts - configMap: - name: auto-update-scripts \ No newline at end of file