Skip to content
This repository has been archived by the owner on Aug 17, 2023. It is now read-only.

[WIP] Add ConfigMap builder #21

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/knative-builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

import fairing
from fairing import builders
from fairing.training import native
from fairing.training import kubernetes

DOCKER_REPOSITORY_NAME = '<your-repository-name>'
fairing.config.set_builder(builders.KnativeBuilder(repository=DOCKER_REPOSITORY_NAME))
Expand All @@ -46,7 +46,7 @@
'tensorflow/mnist/logs/fully_connected_feed/', os.getenv('HOSTNAME', ''))
MODEL_DIR = os.path.join(LOG_DIR, 'model.ckpt')

@native.Training()
@kubernetes.Training()
class MyModel(object):
def train(self):
self.data_sets = input_data.read_data_sets(INPUT_DATA_DIR)
Expand Down
4 changes: 2 additions & 2 deletions examples/simple-training/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

import fairing
from fairing import builders
from fairing.training import native
from fairing.training import kubernetes

DOCKER_REPOSITORY_NAME = '<your-repository-name>'
fairing.config.set_builder(builders.DockerBuilder(DOCKER_REPOSITORY_NAME))
Expand All @@ -47,7 +47,7 @@
'tensorflow/mnist/logs/fully_connected_feed/', os.getenv('HOSTNAME', ''))
MODEL_DIR = os.path.join(LOG_DIR, 'model.ckpt')

@native.Training()
@kubernetes.Training()
class MyModel(object):
def train(self):
self.data_sets = input_data.read_data_sets(INPUT_DATA_DIR)
Expand Down
8 changes: 8 additions & 0 deletions fairing/backend/kubernetes/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class KubeManager(object):

def __init__(self):
config.load_kube_config()

def create_config_map(self, namespace, config_map):
"""Creates a V1ConfigMap in the specified namespace"""
api_instance = client.CoreV1Api()
api_instance.create_namespaced_config_map(
namespace,
config_map
)

def create_job(self, namespace, job):
"""Creates a V1Job in the specified namespace"""
Expand Down
14 changes: 5 additions & 9 deletions fairing/builders/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@
class BuilderInterface(object):

@abc.abstractmethod
def execute(self):
"""Will be called when the build needs to start"""
def execute(self, namespace, job_id, base_image):
"""Will be called when the build needs to start,
This method should return a V1PodSpec with the correct image set.
This is also where the builder should set the environment variables
and volume/volumeMounts that it may need to work"""
raise NotImplementedError('BuilderInterface.execute')

@abc.abstractmethod
def generate_pod_spec(self):
"""This method should return a V1PodSpec with the correct image set.
This is also where the builder should set the environment variables
and volume/volumeMounts that it may need to work"""
raise NotImplementedError('BuilderInterface.generate_pod_spec')
61 changes: 61 additions & 0 deletions fairing/builders/config_maps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os
import subprocess
import tempfile
import shutil

from kubernetes import client

from fairing import notebook_helper
from .builder import BuilderInterface
from fairing.backend import kubernetes


class ConfigMapBuilder(BuilderInterface):

def __init__(self, notebook_name=None):
if notebook_name is None:
self.notebook_name = notebook_helper.get_notebook_name()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? If not, we should make notebook_name required.

Copy link
Contributor Author

@wbuchwalter wbuchwalter Nov 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works in standalone notebooks, not with JupyterHub.
I can improve the error handling in the helper to display an explicit error when user JH such as: "notebook_name is required when using JupyterHub"

else:
self.notebook_name = notebook_name

def execute(self, namespace, job_id, base_image):
nb_full_path = os.path.join(os.getcwd(), self.notebook_name)
temp_dir = tempfile.mkdtemp()
code_path = os.path.join(temp_dir, 'code.py')
try:
cmd = "jupyter nbconvert --to python {nb_path} --output {output}"
.format(
nb_path=nb_full_path,
output=code_path
).split()

subprocess.check_call(cmd)
with open(code_path, 'rb') as f:
code = f.read()
finally:
shutil.rmtree(temp_dir, ignore_errors=True)

client.V1ConfigMap(
api_version="v1",
data={"code.py": code},
metadata=client.V1ObjectMeta(name=self.job_id)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is job_id a parameter or member? Who sets it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

job_id is generated automatically by the Deployment and is used to ensure we get the correct logs.

)
k8s = kubernetes.KubeManager()
k8s.create_config_map(namespace, config_map)
return self._generate_pod_spec(job_id, base_image)

def _generate_pod_spec(self, job_id, base_image):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it would be this PR but would be great to provide this pod-spec using a configmap(that way specifying images, environments, and other mounts is completely upto the user).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be part of the discussion here: #28 and be in it's own PR.

volume_name = 'code'
return client.V1PodSpec(
containers=[client.V1Container(
name='model',
image=base_image,
command="python /code/code.py".split(),
volume_mounts=[client.V1VolumeMount(name=volume_name, mount_path='/code')]
)],
restart_policy='Never',
volumes=client.V1Volume(
name=volume_name,
config_map=client.V1ConfigMapVolumeSource(name=job_id)
)
)
20 changes: 9 additions & 11 deletions fairing/builders/docker_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
from future import standard_library
standard_library.install_aliases()

import shutil
import os
import json
import logging
import sys

from docker import APIClient
from kubernetes import client
Expand All @@ -29,12 +27,10 @@ def __init__(self,
repository,
image_name=DEFAULT_IMAGE_NAME,
image_tag=None,
base_image=None,
dockerfile_path=None):

self.repository = repository
self.image_name = image_name
self.base_image = base_image
self.dockerfile_path = dockerfile_path

if image_tag is None:
Expand All @@ -48,7 +44,7 @@ def __init__(self,
)
self.docker_client = None

def generate_pod_spec(self):
def _generate_pod_spec(self, job_id):
"""return a V1PodSpec initialized with the proper container"""

return client.V1PodSpec(
Expand All @@ -59,15 +55,17 @@ def generate_pod_spec(self):
restart_policy='Never'
)

def execute(self):
def execute(self, namespace, job_id, base_image):
write_dockerfile(
dockerfile_path=self.dockerfile_path,
base_image=self.base_image)
base_image=base_image
)
self.docker_client = APIClient(version='auto')
self.build()
self.publish()
self._build()
self._publish()
return self._generate_pod_spec(job_id)

def build(self):
def _build(self):
logger.warn('Building docker image {}...'.format(self.full_image_name))
bld = self.docker_client.build(
path='.',
Expand All @@ -78,7 +76,7 @@ def build(self):
for line in bld:
self._process_stream(line)

def publish(self):
def _publish(self):
logger.warn('Publishing image {}...'.format(self.full_image_name))
for line in self.docker_client.push(self.full_image_name, stream=True):
self._process_stream(line)
Expand Down
7 changes: 1 addition & 6 deletions fairing/builders/dockerfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_default_base_image():
return '{uname}/fairing:dev'.format(uname=uname)
return 'library/python:3.6'

def generate_dockerfile( base_image):
def generate_dockerfile(base_image):
if base_image is None:
base_image = get_default_base_image()

Expand All @@ -70,11 +70,6 @@ def get_mandatory_steps():
]
return steps

# def get_env_steps(self, env):
# if env:
# return ["ENV {} {}".format(e['name'], e['value']) for e in env]
# return []

def write_dockerfile(destination='Dockerfile', dockerfile_path=None, base_image=None):
if dockerfile_path is not None:
shutil.copy(dockerfile_path, destination)
Expand Down
37 changes: 18 additions & 19 deletions fairing/builders/knative/knative.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,12 @@ def __init__(self,
repository,
image_name=DEFAULT_IMAGE_NAME,
image_tag=None,
base_image=None,
dockerfile_path=None):

self.repository = repository
self.image_name = image_name
self.base_image = base_image
self.dockerfile_path = dockerfile_path
self.namespace = self.get_current_namespace()
self.namespace = self._get_current_namespace()

if image_tag is None:
self.image_tag = utils.get_unique_tag()
Expand All @@ -43,16 +41,17 @@ def __init__(self,
# Unique build_id to avoid conflicts
self._build_id = utils.get_unique_tag()

def execute(self):
def execute(self, namespace, job_id, base_image):
dockerfile.write_dockerfile(
dockerfile_path=self.dockerfile_path,
base_image=self.base_image
base_image=base_image
)
self.copy_src_to_mount_point()
self.build_and_push()
self._copy_src_to_mount_point()
self._build_and_push()
return self._generate_pod_spec()


def generate_pod_spec(self):
def _generate_pod_spec(self):
"""return a V1PodSpec initialized with the proper container"""
return client.V1PodSpec(
containers=[client.V1Container(
Expand All @@ -66,46 +65,46 @@ def generate_pod_spec(self):
restart_policy='Never'
)

def copy_src_to_mount_point(self):
def _copy_src_to_mount_point(self):
context_dir = os.getcwdu()
dst = os.path.join(self.get_mount_point(), self._build_id)
dst = os.path.join(self._get_mount_point(), self._build_id)
shutil.copytree(context_dir, dst)

def get_mount_point(self):
def _get_mount_point(self):
return os.path.join(os.environ['HOME'], '.fairing/build-contexts/')

def build_and_push(self):
def _build_and_push(self):
logger.warn(
'Building docker image {repository}/{image}:{tag}...'.format(
repository=self.repository,
image=self.image_name,
tag=self.image_tag
)
)
self.authenticate()
self._authenticate()

build_template = self.generate_build_template_resource()
build_template = self._generate_build_template_resource()
build_template.maybe_create()

build = self.generate_build_resource()
build = self._generate_build_resource()
build.create_sync()
# TODO: clean build?

def authenticate(self):
def _authenticate(self):
if utils.is_running_in_k8s():
config.load_incluster_config()
else:
config.load_kube_config()

def get_current_namespace(self):
def _get_current_namespace(self):
if not utils.is_running_in_k8s():
logger.debug("""Fairing does not seem to be running inside
Kubernetes, cannot infer namespace. Using namespaces 'fairing'
instead""")
return 'fairing'
return utils.get_current_k8s_namespace()

def generate_build_template_resource(self):
def _generate_build_template_resource(self):
metadata = client.V1ObjectMeta(
name='fairing-build', namespace=self.namespace)

Expand Down Expand Up @@ -133,7 +132,7 @@ def generate_build_template_resource(self):
parameters=params, steps=steps, volumes=[volume])
return BuildTemplate(metadata=metadata, spec=spec)

def generate_build_resource(self):
def _generate_build_resource(self):
metadata = client.V1ObjectMeta(
name='fairing-build-{}'.format(self.image_name),
namespace=self.namespace)
Expand Down
4 changes: 1 addition & 3 deletions fairing/notebook_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ def get_notebook_name():
if nn['kernel']['id'] == kernel_id:
full_path = nn['notebook']['path']
return os.path.basename(full_path)

return f

def is_in_notebook():
try:
ipykernel.get_connection_info()
except RuntimeError:
return False
return True
return True
12 changes: 6 additions & 6 deletions fairing/training/kubeflow/deployment.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from kubernetes import client as k8s_client

from ..native import deployment
from ..kubernetes import deployment

class KubeflowDeployment(deployment.NativeDeployment):
class KubeflowDeployment(deployment.KubernetesDeployment):

def __init__(self, namespace, runs, distribution):
super(KubeflowDeployment, self).__init__(namespace, runs)
def __init__(self, namespace, runs, distribution, base_image=None):
super(KubeflowDeployment, self).__init__(namespace, runs, base_image)
self.distribution = distribution

def deploy(self):
Expand Down Expand Up @@ -37,7 +37,7 @@ def generate_job(self, pod_template_spec):
tf_job = {}
tf_job['kind'] = 'TFJob'
tf_job['apiVersion'] = 'kubeflow.org/v1alpha2'
tf_job['metadata'] = k8s_client.V1ObjectMeta(name=self.name)
tf_job['metadata'] = k8s_client.V1ObjectMeta(name=self.job_id)
tf_job['spec'] = spec

return tf_job
Expand All @@ -49,4 +49,4 @@ def set_container_name(self, pod_template_spec):

def get_logs(self):
selector='tf-replica-index=0,tf-replica-type=worker'
self.backend.log(self.name, self.namespace, selector)
self.backend.log(self.job_id, self.namespace, selector)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
from future import standard_library
standard_library.install_aliases()

from .deployment import NativeDeployment
from .runtime import BasicNativeRuntime
from .deployment import KubernetesDeployment
from .runtime import BasicKubernetesRuntime
from .decorators import Training
Loading