Add end2end test for Xgboost housing example (#493)

* Add e2e test for xgboost housing example

* fix typo

add ks apply

add [

modify example to trigger tests

add prediction test

add xgboost ks param

rename the job name without _

use - instead of _

libson params

rm redudent component

rename component in prow config

add ames-hoursing-env

use - for all names

use _ for params names

use xgboost_ames_accross

rename component name

shorten the name

change deploy-test command

change to xgboost-
namespace

init ks app

fix type

add confest.py

change path

change deploy command

change dep

change the query URL for seldon

add ks_app with seldon lib

update ks_app

use ks init only

rerun

change to kf-v0-4-n00 cluster

add ks_app

use ks-13

remove --namespace

use kubeflow as namespace

delete seldon deployment

simplify ks_app

retry on 503

fix typo

query 1285

move deletion after prediction

wait 10s

always retry till 10 mins

move check to retry

 fix pylint

move  clean-up to the delete template

* set up xgboost component

* check in ks component& run it directly

* change comments

* add comment on why use 'ks delete'

* add two modules to pylint whitelist

* ignore tf_operator/py

* disable pylint per line

* reorder import
This commit is contained in:
Zhenghui Wang 2019-02-12 06:37:05 -08:00 committed by Kubernetes Prow Robot
parent 329c53cea5
commit 74378a2990
37 changed files with 7421 additions and 11 deletions

View File

@ -28,8 +28,8 @@ import logging
import os
from kubernetes import client as k8s_client
from py import tf_job_client
from py import test_runner
from py import tf_job_client #pylint: disable=no-name-in-module
from py import test_runner #pylint: disable=no-name-in-module
from kubeflow.testing import ks_util
from kubeflow.testing import test_util

View File

@ -23,7 +23,7 @@ import logging
import os
from kubernetes import client as k8s_client
from py import test_runner
from py import test_runner #pylint: disable=no-name-in-module
from kubeflow.testing import ks_util
from kubeflow.testing import test_util

View File

@ -28,8 +28,8 @@ import logging
import os
from kubernetes import client as k8s_client
from py import tf_job_client
from py import test_runner
from py import tf_job_client #pylint: disable=no-name-in-module
from py import test_runner #pylint: disable=no-name-in-module
from kubeflow.testing import ks_util
from kubeflow.testing import test_util

View File

@ -18,9 +18,9 @@ Manually running the test
import os
import mnist_client
from py import test_runner #pylint: disable=no-name-in-module
from py import test_runner
import mnist_client
from kubeflow.testing import test_util

View File

@ -6,7 +6,7 @@ workflows:
name: examples-e2e
job_types:
- presubmit
# E2E test for code_search example
- app_dir: kubeflow/examples/test/workflows
component: code_search
@ -16,7 +16,7 @@ workflows:
- postsubmit
include_dirs:
- code_search/*
# E2E test for mnist example
- app_dir: kubeflow/examples/test/workflows
component: mnist
@ -38,6 +38,19 @@ workflows:
- postsubmit
include_dirs:
- github_issue_summarization/*
# E2E test for xgboost housing example
- app_dir: kubeflow/examples/test/workflows
component: xgboost_ames_housing
name: xgboost
job_types:
- periodic
- presubmit
- postsubmit
include_dirs:
- xgboost_ames_housing/*
# Image Auto Release workflows.
# The workflows below are related to auto building our Docker images.
# We have separate pre/postsubmit jobs because we want to use different

View File

@ -24,6 +24,12 @@
namespace: "kubeflow-test-infra",
prow_env: "BUILD_NUMBER=997a,BUILD_ID=997a,JOB_NAME=kubeflow-examples-presubmit-test,JOB_TYPE=presubmit,PULL_NUMBER=374,REPO_NAME=examples,REPO_OWNER=kubeflow",
},
xgboost_ames_housing: {
bucket: "kubeflow-ci_temp",
name: "kubeflow-xgboost-ames-housing",
namespace: "kubeflow-test-infra",
prow_env: "BUILD_NUMBER=997a,BUILD_ID=997a,JOB_NAME=kubeflow-examples-presubmit-test,JOB_TYPE=presubmit,PULL_NUMBER=374,REPO_NAME=examples,REPO_OWNER=kubeflow",
},
workflows: {
bucket: "kubeflow-ci_temp",
name: "kubeflow-examples-presubmit-test-374-6e32",

View File

@ -0,0 +1,514 @@
// Test workflow for XGBoost Housing example.
//
local env = std.extVar("__ksonnet/environments");
local overrides = std.extVar("__ksonnet/params").components.xgboost_ames_housing;
local k = import "k.libsonnet";
local util = import "util.libsonnet";
// Define default params and then combine them with any overrides
local defaultParams = {
// local nfsVolumeClaim: "kubeflow-testing",
nfsVolumeClaim: "nfs-external",
// The name to use for the volume to use to contain test data.
dataVolume: "kubeflow-test-volume",
// Default step image:
stepImage: "gcr.io/kubeflow-ci/test-worker/test-worker:v20190116-b7abb8d-e3b0c4",
// Which Kubeflow cluster to use for running TFJobs on.
kfProject: "kubeflow-ci",
kfZone: "us-east1-d",
kfCluster: "kf-v0-4-n00",
// The bucket where the model should be written
// This needs to be writable by the GCP service account in the Kubeflow cluster (not the test cluster)
modelBucket: "kubeflow-ci_temp",
// Whether to delete the namespace at the end.
// Leaving the namespace around can be useful for debugging.
//
// TODO(jlewi): We should consider running a cronjob to GC namespaces.
// But if we leave namespaces up; then we end up leaving the servers up which
// uses up CPU.
//
deleteNamespace: true,
};
local params = defaultParams + overrides;
local prowEnv = util.parseEnv(params.prow_env);
// Create a dictionary of the different prow variables so we can refer to them in the workflow.
//
// Important: We want to initialize all variables we reference to some value. If we don't
// and we reference a variable which doesn't get set then we get very hard to debug failure messages.
// In particular, we've seen problems where if we add a new environment and evaluate one component eg. "workflows"
// and another component e.g "code_search.jsonnet" doesn't have a default value for BUILD_ID then ksonnet
// fails because BUILD_ID is undefined.
local prowDict = {
BUILD_ID: "notset",
BUILD_NUMBER: "notset",
REPO_OWNER: "notset",
REPO_NAME: "notset",
JOB_NAME: "notset",
JOB_TYPE: "notset",
PULL_NUMBER: "notset",
} + util.listOfDictToMap(prowEnv);
local bucket = params.bucket;
// mountPath is the directory where the volume to store the test data
// should be mounted.
local mountPath = "/mnt/" + "test-data-volume";
// testDir is the root directory for all data for a particular test run.
local testDir = mountPath + "/" + params.name;
// outputDir is the directory to sync to GCS to contain the output for this job.
local outputDir = testDir + "/output";
local artifactsDir = outputDir + "/artifacts";
// Source directory where all repos should be checked out
local srcRootDir = testDir + "/src";
// The directory containing the kubeflow/kubeflow repo
local srcDir = srcRootDir + "/" + prowDict.REPO_OWNER + "/" + prowDict.REPO_NAME;
// These variables control where the docker images get pushed and what
// tag to use
local imageBase = "gcr.io/kubeflow-ci/xgboost_ames_housing";
local imageTag = "build-" + prowDict["BUILD_ID"];
local trainerImage = imageBase + "/model:" + imageTag;
// Directory where model should be stored.
local modelDir = "gs://" + params.modelBucket + "/xgboost_ames_housing/models/" + prowDict["BUILD_ID"];
// value of KUBECONFIG environment variable. This should be a full path.
local kubeConfig = testDir + "/.kube/kubeconfig";
// Namespace where tests should run
local testNamespace = "xgboost-ames-housing-" + prowDict["BUILD_ID"];
// The directory within the kubeflow_testing submodule containing
// py scripts to use.
local kubeflowTestingPy = srcRootDir + "/kubeflow/testing/py";
local tfOperatorPy = srcRootDir + "/kubeflow/tf-operator";
// Workflow template is the name of the workflow template; typically the name of the ks component.
// This is used as a label to make it easy to identify all Argo workflows created from a given
// template.
local workflow_template = "xgboost_ames_housing";
// Build template is a template for constructing Argo step templates.
//
// step_name: Name for the template
// command: List to pass as the container command.
//
// We customize the defaults for each step in the workflow by modifying
// buildTemplate.argoTemplate
local buildTemplate = {
// name & command variables should be overwritten for every test.
// Other variables can be changed per step as needed.
// They are hidden because they shouldn't be included in the Argo template
name: "",
command:: "",
image: params.stepImage,
workingDir:: null,
env_vars:: [],
side_cars: [],
pythonPath: kubeflowTestingPy,
activeDeadlineSeconds: 1800, // Set 30 minute timeout for each template
local template = self,
// Actual template for Argo
argoTemplate: {
name: template.name,
metadata: {
labels: prowDict + {
workflow: params.name,
workflow_template: workflow_template,
step_name: template.name,
},
},
container: {
command: template.command,
name: template.name,
image: template.image,
workingDir: template.workingDir,
env: [
{
// Add the source directories to the python path.
name: "PYTHONPATH",
value: template.pythonPath,
},
{
name: "GOOGLE_APPLICATION_CREDENTIALS",
value: "/secret/gcp-credentials/key.json",
},
{
name: "GITHUB_TOKEN",
valueFrom: {
secretKeyRef: {
name: "github-token",
key: "github_token",
},
},
},
{
// We use a directory in our NFS share to store our kube config.
// This way we can configure it on a single step and reuse it on subsequent steps.
name: "KUBECONFIG",
value: kubeConfig,
},
] + prowEnv + template.env_vars,
volumeMounts: [
{
name: params.dataVolume,
mountPath: mountPath,
},
{
name: "github-token",
mountPath: "/secret/github-token",
},
{
name: "gcp-credentials",
mountPath: "/secret/gcp-credentials",
},
],
},
},
}; // buildTemplate
// Create a list of dictionary.
// Each item is a dictionary describing one step in the graph.
local dagTemplates = [
{
template: buildTemplate {
name: "checkout",
command:
["/usr/local/bin/checkout.sh", srcRootDir],
env_vars: [{
name: "EXTRA_REPOS",
// TODO(jlewi): Pin to commit on master when #281 is checked in.
value: "kubeflow/testing@HEAD:281;kubeflow/tf-operator@HEAD",
}],
},
dependencies: null,
}, // checkout
{
// TODO(https://github.com/kubeflow/testing/issues/257): Create-pr-symlink
// should be done by run_e2e_workflow.py
template: buildTemplate {
name: "create-pr-symlink",
command: [
"python",
"-m",
"kubeflow.testing.prow_artifacts",
"--artifacts_dir=" + outputDir,
"create_pr_symlink",
"--bucket=" + params.bucket,
],
}, // create-pr-symlink
dependencies: ["checkout"],
}, // create-pr-symlink
{
// Submit a GCB job to build the images
template: buildTemplate {
name: "build-images",
command: util.buildCommand([
[
"gcloud",
"auth",
"activate-service-account",
"--key-file=${GOOGLE_APPLICATION_CREDENTIALS}",
],
[
"make",
"build-seldon-image",
"IMG=" + imageBase,
"TAG=" + imageTag,
"SOURCE=" + "../seldon_serve"
]]
),
workingDir: srcDir + "/xgboost_ames_housing/test",
},
dependencies: ["checkout"],
}, // build-images
{
// Configure KUBECONFIG
template: buildTemplate {
name: "get-kubeconfig",
command: util.buildCommand([
[
"gcloud",
"auth",
"activate-service-account",
"--key-file=${GOOGLE_APPLICATION_CREDENTIALS}",
],
[
"gcloud",
"--project=" + params.kfProject,
"container",
"clusters",
"get-credentials",
"--zone=" + params.kfZone,
params.kfCluster,
]]
),
},
dependencies: ["checkout"],
}, // get-kubeconfig
{
// Create the namespace
// TODO(jlewi): We should add some sort of retry.
template: buildTemplate {
name: "create-namespace",
command: util.buildCommand([
[
"echo",
"KUBECONFIG=",
"${KUBECONFIG}",
],
[
"gcloud",
"auth",
"activate-service-account",
"--key-file=${GOOGLE_APPLICATION_CREDENTIALS}",
],
[
"kubectl",
"config" ,
"current-context",
],
[
"kubectl",
"create",
"namespace",
testNamespace,
],
# Copy the GCP secret from the kubeflow namespace to the test namespace
[
srcDir + "/test/copy_secret.sh",
"kubeflow",
testNamespace,
"user-gcp-sa",
]]
),
},
dependencies: ["get-kubeconfig"],
}, // create-namespace
{
template: buildTemplate {
name: "deploy-seldon",
command: util.buildCommand([[
"ks-13",
"param",
"set",
"xgboost",
"name",
"xgboost-ames-" + prowDict["BUILD_ID"],
"--env=default",
],
[
"ks-13",
"param",
"set",
"xgboost",
"image",
imageBase + ":" + imageTag,
"--env=default"
],
[
"ks-13",
"apply",
"default",
"-c",
"xgboost",
]]),
workingDir: srcDir + "/xgboost_ames_housing/ks_app",
},
dependencies: ["build-images"],
}, // deploy-seldon
{
template: buildTemplate {
name: "predict-test",
command: [
"pytest",
"predict_test.py",
"-s",
// Test timeout in seconds.
"--timeout=500",
"--junitxml=" + artifactsDir + "/junit_predict-test.xml",
"--namespace=kubeflow",
"--service=xgboost-ames-" + prowDict["BUILD_ID"],
],
workingDir: srcDir + "/xgboost_ames_housing/test",
},
dependencies: ["deploy-seldon"],
}, // predict-test
];
// Dag defines the tasks in the graph
local dag = {
name: "e2e",
// Construct tasks from the templates
// we will give the steps the same name as the template
dag: {
tasks: util.toArgoTaskList(dagTemplates),
},
}; // dag
// Define templates for the steps to be performed when the
// test exits
local deleteTemplates = if params.deleteNamespace then
[
{
// Delete the namespace
// TODO(jlewi): We should add some sort of retry.
template: buildTemplate {
name: "delete-namespace",
command: util.buildCommand([
[
"gcloud",
"auth",
"activate-service-account",
"--key-file=${GOOGLE_APPLICATION_CREDENTIALS}",
],
[
"kubectl",
"delete",
"namespace",
testNamespace,
]]
),
},
}, // delete-namespace
] else [];
local exitTemplates =
deleteTemplates +
[
{
// Copy artifacts to GCS for gubernator.
// TODO(https://github.com/kubeflow/testing/issues/257): Create-pr-symlink
// should be done by run_e2e_workflow.py
template: buildTemplate {
name: "copy-artifacts",
command: [
"python",
"-m",
"kubeflow.testing.prow_artifacts",
"--artifacts_dir=" + outputDir,
"copy_artifacts",
"--bucket=" + bucket,
],
}, // copy-artifacts,
},
{
// Delete the seldon deployment
// TODO(zhenghuiwang): Seldon doesn't deploy seldon deployments into a different
// namespace (https://github.com/kubeflow/kubeflow/issues/1712). After this
// issue is fixed, we don't need to use `ks delete`.
template: buildTemplate {
name: "delete-seldon-deployment",
command: [
"ks-13",
"delete",
"default",
"-c",
"xgboost",
],
workingDir: srcDir + "/xgboost_ames_housing/ks_app",
},
}, // delete-seldon-deployment
{
// Delete the test directory in NFS.
// TODO(https://github.com/kubeflow/testing/issues/256): Use an external process to do this.
template:
buildTemplate {
name: "test-dir-delete",
command: [
"rm",
"-rf",
testDir,
],
argoTemplate+: {
retryStrategy: {
limit: 3,
},
},
}, // test-dir-delete
dependencies: ["copy-artifacts"] + if params.deleteNamespace then ["delete-namespace"] else [],
},
];
// Create a DAG representing the set of steps to execute on exit
local exitDag = {
name: "exit-handler",
// Construct tasks from the templates
// we will give the steps the same name as the template
dag: {
tasks: util.toArgoTaskList(exitTemplates),
},
};
// A list of templates for the actual steps
local stepTemplates = std.map(function(i) i.template.argoTemplate
, dagTemplates) +
std.map(function(i) i.template.argoTemplate
, exitTemplates);
// Define the Argo Workflow.
local workflow = {
apiVersion: "argoproj.io/v1alpha1",
kind: "Workflow",
metadata: {
name: params.name,
namespace: env.namespace,
labels: prowDict + {
workflow: params.name,
workflow_template: workflow_template,
},
},
spec: {
entrypoint: "e2e",
// Have argo garbage collect old workflows otherwise we overload the API server.
ttlSecondsAfterFinished: 7 * 24 * 60 * 60,
volumes: [
{
name: "github-token",
secret: {
secretName: "github-token",
},
},
{
name: "gcp-credentials",
secret: {
secretName: "kubeflow-testing-credentials",
},
},
{
name: params.dataVolume,
persistentVolumeClaim: {
claimName: params.nfsVolumeClaim,
},
},
], // volumes
// onExit specifies the template that should always run when the workflow completes.
onExit: "exit-handler",
// The templates will be a combination of the templates
// defining the dags executed by Argo as well as the templates
// for the individual steps.
templates: [dag, exitDag] + stepTemplates, // templates
}, // spec
}; // workflow
std.prune(k.core.v1.list.new([workflow]))

View File

@ -17,6 +17,11 @@ local envParams = params + {
name: 'jlewi-mnist-test-479-0118-110631',
prow_env: 'JOB_NAME=mnist-test,JOB_TYPE=presubmit,REPO_NAME=examples,REPO_OWNER=kubeflow,BUILD_NUMBER=0118-110631,BUILD_ID=0118-110631,PULL_NUMBER=479',
},
xgboost_ames_housing+: {
namespace: 'kubeflow-test-infra',
name: 'xgboost-ames-housing-test-479-0118-110631',
prow_env: 'JOB_NAME=mnist-test,JOB_TYPE=presubmit,REPO_NAME=examples,REPO_OWNER=kubeflow,BUILD_NUMBER=0118-110631,BUILD_ID=0118-110631,PULL_NUMBER=489',
},
},
};
@ -25,4 +30,4 @@ local envParams = params + {
[x]: envParams.components[x] + globals
for x in std.objectFields(envParams.components)
},
}
}

View File

@ -0,0 +1,4 @@
/lib
/.ksonnet/registries
/app.override.yaml
/.ks_environment

View File

@ -0,0 +1,20 @@
apiVersion: 0.2.0
environments:
default:
destination:
namespace: kubeflow
server: https://35.237.212.62
k8sVersion: v1.11.5
path: default
kind: ksonnet.io/app
libraries:
seldon:
name: seldon
registry: kubeflow
version: ""
name: ks_app
registries:
kubeflow:
protocol: fs
uri: /home/jlewi/git_kubeflow/kubeflow
version: 0.0.1

View File

@ -0,0 +1,26 @@
{
global: {},
components: {
// Component-level parameters, defined initially from 'ks prototype use ...'
// Each object below should correspond to a component in the components/ directory
seldon: {
apifeServiceType: "NodePort",
grpcMaxMessageSize: "4194304",
name: "seldon",
operatorJavaOpts: "null",
operatorSpringOpts: "null",
seldonVersion: "0.2.3",
withAmbassador: "false",
withApife: "false",
withRbac: "true",
},
"xgboost": {
endpoint: "REST",
image: "image-path",
imagePullSecret: "null",
name: "xgboost",
pvcName: "null",
replicas: 1,
},
},
}

View File

@ -0,0 +1,75 @@
local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components.seldon;
local k = import "k.libsonnet";
local core = import "kubeflow/seldon/core.libsonnet";
local seldonVersion = params.seldonVersion;
local name = params.name;
local namespace = env.namespace;
local withRbac = params.withRbac;
local withApife = params.withApife;
local withAmbassador = params.withAmbassador;
// APIFE
local apifeImage = "seldonio/apife:" + seldonVersion;
local apifeServiceType = params.apifeServiceType;
local grpcMaxMessageSize = params.grpcMaxMessageSize;
// Cluster Manager (The CRD Operator)
local operatorImage = "seldonio/cluster-manager:" + seldonVersion;
local operatorSpringOptsParam = params.operatorSpringOpts;
local operatorSpringOpts = if operatorSpringOptsParam != "null" then operatorSpringOptsParam else "";
local operatorJavaOptsParam = params.operatorJavaOpts;
local operatorJavaOpts = if operatorJavaOptsParam != "null" then operatorJavaOptsParam else "";
// Engine
local engineImage = "seldonio/engine:" + seldonVersion;
// APIFE
local apife = [
core.parts(name, namespace, seldonVersion).apife(apifeImage, withRbac, grpcMaxMessageSize),
core.parts(name, namespace, seldonVersion).apifeService(apifeServiceType),
];
local rbac2 = [
core.parts(name, namespace, seldonVersion).rbacServiceAccount(),
core.parts(name, namespace, seldonVersion).rbacClusterRole(),
core.parts(name, namespace, seldonVersion).rbacRole(),
core.parts(name, namespace, seldonVersion).rbacRoleBinding(),
core.parts(name, namespace, seldonVersion).rbacClusterRoleBinding(),
];
local rbac1 = [
core.parts(name, namespace, seldonVersion).rbacServiceAccount(),
core.parts(name, namespace, seldonVersion).rbacRoleBinding(),
];
local rbac = if std.startsWith(seldonVersion, "0.1") then rbac1 else rbac2;
// Core
local coreComponents = [
core.parts(name, namespace, seldonVersion).deploymentOperator(engineImage, operatorImage, operatorSpringOpts, operatorJavaOpts, withRbac),
core.parts(name, namespace, seldonVersion).redisDeployment(),
core.parts(name, namespace, seldonVersion).redisService(),
core.parts(name, namespace, seldonVersion).crd(),
];
//Ambassador
local ambassadorRbac = [
core.parts(name, namespace, seldonVersion).rbacAmbassadorRole(),
core.parts(name, namespace, seldonVersion).rbacAmbassadorRoleBinding(),
];
local ambassador = [
core.parts(name, namespace, seldonVersion).ambassadorDeployment(),
core.parts(name, namespace, seldonVersion).ambassadorService(),
];
local l1 = if withRbac == "true" then rbac + coreComponents else coreComponents;
local l2 = if withApife == "true" then l1 + apife else l1;
local l3 = if withAmbassador == "true" && withRbac == "true" then l2 + ambassadorRbac else l2;
local l4 = if withAmbassador == "true" then l3 + ambassador else l3;
l4

View File

@ -0,0 +1,95 @@
local env = std.extVar("__ksonnet/environments");
local params = std.extVar("__ksonnet/params").components["xgboost"];
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment = {
apiVersion: "machinelearning.seldon.io/v1alpha2",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
deployment_version: "v1",
project_name: params.name,
},
name: params.name,
predictors: [
{
annotations: {
predictor_version: "v1",
},
componentSpecs: [{
spec: {
containers: [
{
image: params.image,
imagePullPolicy: "IfNotPresent",
name: params.name,
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
}],
graph: {
children: [
],
endpoint: {
type: params.endpoint,
},
name: params.name,
type: "MODEL",
},
name: params.name,
replicas: params.replicas,
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,4 @@
local components = std.extVar("__ksonnet/components");
components + {
// Insert user-specified overrides here.
}

View File

@ -0,0 +1,2 @@
{
}

View File

@ -0,0 +1,8 @@
local base = import "base.libsonnet";
// uncomment if you reference ksonnet-lib
// local k = import "k.libsonnet";
base + {
// Insert user-specified overrides here. For example if a component is named \"nginx-deployment\", you might have something like:\n")
// "nginx-deployment"+: k.deployment.mixin.metadata.labels({foo: "bar"})
}

View File

@ -0,0 +1,18 @@
local params = std.extVar('__ksonnet/params');
local globals = import 'globals.libsonnet';
local envParams = params + {
components+: {
xgboost+: {
name: 'xgboost-ames-1300',
image: 'gcr.io/kubeflow-ci/xgboost_ames_housing:build-1300',
replicas: 1,
},
},
};
{
components: {
[x]: envParams.components[x] + globals
for x in std.objectFields(envParams.components)
},
}

View File

@ -0,0 +1,256 @@
local k = import "k.libsonnet";
local deployment = k.extensions.v1beta1.deployment;
local container = k.apps.v1beta1.deployment.mixin.spec.template.spec.containersType;
local service = k.core.v1.service.mixin;
local serviceAccountMixin = k.core.v1.serviceAccount.mixin;
local clusterRoleBindingMixin = k.rbac.v1beta1.clusterRoleBinding.mixin;
local clusterRoleBinding = k.rbac.v1beta1.clusterRoleBinding;
local roleBindingMixin = k.rbac.v1beta1.roleBinding.mixin;
local roleBinding = k.rbac.v1beta1.roleBinding;
local roleMixin = k.rbac.v1beta1.role.mixin;
local serviceAccount = k.core.v1.serviceAccount;
local crdDefn = import "crd.libsonnet";
local seldonTemplate2 = import "json/template_0.2.json";
local seldonTemplate1 = import "json/template_0.1.json";
local getOperatorDeployment(x) = std.endsWith(x.metadata.name, "seldon-cluster-manager");
local getApifeDeployment(x) = std.endsWith(x.metadata.name, "seldon-apiserver") && x.kind == "Deployment";
local getApifeService(x) = std.endsWith(x.metadata.name, "seldon-apiserver") && x.kind == "Service";
local getRedisDeployment(x) = std.endsWith(x.metadata.name, "redis") && x.kind == "Deployment";
local getRedisService(x) = std.endsWith(x.metadata.name, "redis") && x.kind == "Service";
local getServiceAccount(x) = x.kind == "ServiceAccount";
local getClusterRole(x) = x.kind == "ClusterRole";
local getClusterRoleBinding(x) = x.kind == "ClusterRoleBinding";
local getRoleBinding(x) = x.kind == "RoleBinding" && x.roleRef.name == "seldon-local";
local getAmbassadorRoleBinding(x) = x.kind == "RoleBinding" && x.roleRef.name == "ambassador";
local getSeldonRole(x) = x.metadata.name == "seldon-local" && x.kind == "Role";
local getAmbassadorRole(x) = x.metadata.name == "ambassador" && x.kind == "Role";
local getAmbassadorDeployment(x) = std.endsWith(x.metadata.name, "ambassador") && x.kind == "Deployment";
local getAmbassadorService(x) = std.endsWith(x.metadata.name, "ambassador") && x.kind == "Service";
local getEnvNotRedis(x) = x.name != "SELDON_CLUSTER_MANAGER_REDIS_HOST";
{
parts(name, namespace, seldonVersion)::
local seldonTemplate = if std.startsWith(seldonVersion, "0.1") then seldonTemplate1 else seldonTemplate2;
{
apife(apifeImage, withRbac, grpcMaxMessageSize)::
local baseApife = std.filter(getApifeDeployment, seldonTemplate.items)[0];
local env = [
{ name: "SELDON_CLUSTER_MANAGER_REDIS_HOST", value: name + "-redis" },
];
local env2 = std.filter(getEnvNotRedis, baseApife.spec.template.spec.containers[0].env);
local c = baseApife.spec.template.spec.containers[0] +
container.withImage(apifeImage) +
container.withEnv(env + env2) +
container.withImagePullPolicy("IfNotPresent");
local labels = {
"app.kubernetes.io/name": name,
heritage: "ksonnet",
release: name,
};
local apiFeBase1 =
baseApife +
deployment.mixin.metadata.withName(name + "-seldon-apiserver") +
deployment.mixin.metadata.withNamespace(namespace) +
deployment.mixin.metadata.withLabelsMixin(labels) +
deployment.mixin.spec.template.spec.withContainers([c]);
local extraAnnotations = { "seldon.io/grpc-max-message-size": grpcMaxMessageSize };
// Ensure labels copied to enclosed parts
local apiFeBase = apiFeBase1 +
deployment.mixin.spec.selector.withMatchLabels(apiFeBase1.metadata.labels) +
deployment.mixin.spec.template.metadata.withLabels(apiFeBase1.metadata.labels) +
deployment.mixin.spec.template.metadata.withAnnotationsMixin(extraAnnotations);
if withRbac == "true" then
apiFeBase +
deployment.mixin.spec.template.spec.withServiceAccountName("seldon")
else
apiFeBase,
apifeService(serviceType)::
local apifeService = std.filter(getApifeService, seldonTemplate.items)[0];
local labels = { "app.kubernetes.io/name": name };
apifeService +
service.metadata.withName(name + "-seldon-apiserver") +
service.metadata.withNamespace(namespace) +
service.metadata.withLabelsMixin(labels) +
service.spec.withType(serviceType),
deploymentOperator(engineImage, clusterManagerImage, springOpts, javaOpts, withRbac):
local op = std.filter(getOperatorDeployment, seldonTemplate.items)[0];
local env = [
{ name: "JAVA_OPTS", value: javaOpts },
{ name: "SPRING_OPTS", value: springOpts },
{ name: "ENGINE_CONTAINER_IMAGE_AND_VERSION", value: engineImage },
{ name: "ENGINE_CONTAINER_IMAGE_PULL_POLICY", value: "IfNotPresent" },
{ name: "SELDON_CLUSTER_MANAGER_REDIS_HOST", value: name + "-redis" },
{ name: "SELDON_CLUSTER_MANAGER_POD_NAMESPACE", valueFrom: { fieldRef: { apiVersion: "v1", fieldPath: "metadata.namespace" } } },
];
local c = op.spec.template.spec.containers[0] +
container.withImage(clusterManagerImage) +
container.withEnv(env) +
container.withImagePullPolicy("IfNotPresent");
local labels = {
"app.kubernetes.io/name": name,
heritage: "ksonnet",
release: name,
};
local depOp1 = op +
deployment.mixin.metadata.withName(name + "-seldon-cluster-manager") +
deployment.mixin.metadata.withNamespace(namespace) +
deployment.mixin.metadata.withLabelsMixin(labels) +
deployment.mixin.spec.template.spec.withContainers([c]);
// Ensure labels copied to enclosed parts
local depOp = depOp1 +
deployment.mixin.spec.selector.withMatchLabels(depOp1.metadata.labels) +
deployment.mixin.spec.template.metadata.withLabels(depOp1.metadata.labels);
if withRbac == "true" then
depOp +
deployment.mixin.spec.template.spec.withServiceAccountName("seldon")
else
depOp,
redisDeployment():
local redisDeployment = std.filter(getRedisDeployment, seldonTemplate.items)[0];
local labels = {
app: name + "-redis-app",
"app.kubernetes.io/name": name,
heritage: "ksonnet",
release: name,
};
local redisDeployment1 = redisDeployment +
deployment.mixin.metadata.withName(name + "-redis") +
deployment.mixin.metadata.withNamespace(namespace) +
deployment.mixin.metadata.withLabelsMixin(labels);
redisDeployment1 +
deployment.mixin.spec.selector.withMatchLabels(redisDeployment1.metadata.labels) +
deployment.mixin.spec.template.metadata.withLabels(redisDeployment1.metadata.labels),
redisService():
local redisService = std.filter(getRedisService, seldonTemplate.items)[0];
local labels = { "app.kubernetes.io/name": name };
redisService +
service.metadata.withName(name + "-redis") +
service.metadata.withNamespace(namespace) +
service.metadata.withLabelsMixin(labels) +
service.spec.withSelector({ app: name + "-redis-app" }),
rbacServiceAccount():
local rbacServiceAccount = std.filter(getServiceAccount, seldonTemplate.items)[0];
rbacServiceAccount +
serviceAccountMixin.metadata.withNamespace(namespace),
rbacClusterRole():
local clusterRole = std.filter(getClusterRole, seldonTemplate.items)[0];
clusterRole,
rbacRole():
local role = std.filter(getSeldonRole, seldonTemplate.items)[0];
role +
roleMixin.metadata.withNamespace(namespace),
rbacAmbassadorRole():
local role = std.filter(getAmbassadorRole, seldonTemplate.items)[0];
role +
roleMixin.metadata.withNamespace(namespace),
rbacClusterRoleBinding():
local rbacClusterRoleBinding = std.filter(getClusterRoleBinding, seldonTemplate.items)[0];
local subject = rbacClusterRoleBinding.subjects[0]
{ namespace: namespace };
rbacClusterRoleBinding +
clusterRoleBindingMixin.metadata.withNamespace(namespace) +
clusterRoleBinding.withSubjects([subject]),
rbacRoleBinding():
local rbacRoleBinding = std.filter(getRoleBinding, seldonTemplate.items)[0];
local subject = rbacRoleBinding.subjects[0]
{ namespace: namespace };
rbacRoleBinding +
roleBindingMixin.metadata.withNamespace(namespace) +
roleBinding.withSubjects([subject]),
rbacAmbassadorRoleBinding():
local rbacRoleBinding = std.filter(getAmbassadorRoleBinding, seldonTemplate.items)[0];
local subject = rbacRoleBinding.subjects[0]
{ namespace: namespace };
rbacRoleBinding +
roleBindingMixin.metadata.withNamespace(namespace) +
roleBinding.withSubjects([subject]),
ambassadorDeployment():
local ambassadorDeployment = std.filter(getAmbassadorDeployment, seldonTemplate.items)[0];
ambassadorDeployment +
deployment.mixin.metadata.withName(name + "-ambassador") +
deployment.mixin.metadata.withNamespace(namespace),
ambassadorService():
local ambassadorService = std.filter(getAmbassadorService, seldonTemplate.items)[0];
ambassadorService +
service.metadata.withName(name + "-ambassador") +
service.metadata.withNamespace(namespace),
crd():
if std.startsWith(seldonVersion, "0.1") then crdDefn.crd1() else crdDefn.crd2(),
}, // parts
}

View File

@ -0,0 +1,509 @@
local podTemplateValidation = import "json/pod-template-spec-validation.json";
local k = import "k.libsonnet";
{
crd1()::
{
apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
metadata: {
name: "seldondeployments.machinelearning.seldon.io",
},
spec: {
group: "machinelearning.seldon.io",
names: {
kind: "SeldonDeployment",
plural: "seldondeployments",
shortNames: [
"sdep",
],
singular: "seldondeployment",
},
scope: "Namespaced",
validation: {
openAPIV3Schema: {
properties: {
spec: {
properties: {
annotations: {
description: "The annotations to be updated to a deployment",
type: "object",
},
name: {
type: "string",
},
oauth_key: {
type: "string",
},
oauth_secret: {
type: "string",
},
predictors: {
description: "List of predictors belonging to the deployment",
items: {
properties: {
annotations: {
description: "The annotations to be updated to a predictor",
type: "object",
},
graph: {
properties: {
children: {
items: {
properties: {
children: {
items: {
properties: {
children: {
items: {},
type: "array",
},
endpoint: {
properties: {
service_host: {
type: "string",
},
service_port: {
type: "integer",
},
type: {
enum: [
"REST",
"GRPC",
],
type: "string",
},
},
},
name: {
type: "string",
},
implementation: {
enum: [
"UNKNOWN_IMPLEMENTATION",
"SIMPLE_MODEL",
"SIMPLE_ROUTER",
"RANDOM_ABTEST",
"AVERAGE_COMBINER",
],
type: "string",
},
type: {
enum: [
"UNKNOWN_TYPE",
"ROUTER",
"COMBINER",
"MODEL",
"TRANSFORMER",
"OUTPUT_TRANSFORMER",
],
type: "string",
},
methods: {
type: "array",
items: {
enum: [
"TRANSFORM_INPUT",
"TRANSFORM_OUTPUT",
"ROUTE",
"AGGREGATE",
"SEND_FEEDBACK",
],
type: "string",
},
},
},
},
type: "array",
},
endpoint: {
properties: {
service_host: {
type: "string",
},
service_port: {
type: "integer",
},
type: {
enum: [
"REST",
"GRPC",
],
type: "string",
},
},
},
name: {
type: "string",
},
implementation: {
enum: [
"UNKNOWN_IMPLEMENTATION",
"SIMPLE_MODEL",
"SIMPLE_ROUTER",
"RANDOM_ABTEST",
"AVERAGE_COMBINER",
],
type: "string",
},
type: {
enum: [
"UNKNOWN_TYPE",
"ROUTER",
"COMBINER",
"MODEL",
"TRANSFORMER",
"OUTPUT_TRANSFORMER",
],
type: "string",
},
methods: {
type: "array",
items: {
enum: [
"TRANSFORM_INPUT",
"TRANSFORM_OUTPUT",
"ROUTE",
"AGGREGATE",
"SEND_FEEDBACK",
],
type: "string",
},
},
},
},
type: "array",
},
endpoint: {
properties: {
service_host: {
type: "string",
},
service_port: {
type: "integer",
},
type: {
enum: [
"REST",
"GRPC",
],
type: "string",
},
},
},
name: {
type: "string",
},
implementation: {
enum: [
"UNKNOWN_IMPLEMENTATION",
"SIMPLE_MODEL",
"SIMPLE_ROUTER",
"RANDOM_ABTEST",
"AVERAGE_COMBINER",
],
type: "string",
},
type: {
enum: [
"UNKNOWN_TYPE",
"ROUTER",
"COMBINER",
"MODEL",
"TRANSFORMER",
"OUTPUT_TRANSFORMER",
],
type: "string",
},
methods: {
type: "array",
items: {
enum: [
"TRANSFORM_INPUT",
"TRANSFORM_OUTPUT",
"ROUTE",
"AGGREGATE",
"SEND_FEEDBACK",
],
type: "string",
},
},
},
},
name: {
type: "string",
},
replicas: {
type: "integer",
},
},
},
type: "array",
},
componentSpec: podTemplateValidation,
},
},
},
},
},
version: "v1alpha1",
},
},
crd2()::
{
apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
metadata: {
name: "seldondeployments.machinelearning.seldon.io",
},
spec: {
group: "machinelearning.seldon.io",
names: {
kind: "SeldonDeployment",
plural: "seldondeployments",
shortNames: [
"sdep",
],
singular: "seldondeployment",
},
scope: "Namespaced",
validation: {
openAPIV3Schema: {
properties: {
spec: {
properties: {
annotations: {
description: "The annotations to be updated to a deployment",
type: "object",
},
name: {
type: "string",
},
oauth_key: {
type: "string",
},
oauth_secret: {
type: "string",
},
predictors: {
description: "List of predictors belonging to the deployment",
items: {
properties: {
annotations: {
description: "The annotations to be updated to a predictor",
type: "object",
},
graph: {
properties: {
children: {
items: {
properties: {
children: {
items: {
properties: {
children: {
items: {},
type: "array",
},
endpoint: {
properties: {
service_host: {
type: "string",
},
service_port: {
type: "integer",
},
type: {
enum: [
"REST",
"GRPC",
],
type: "string",
},
},
},
name: {
type: "string",
},
implementation: {
enum: [
"UNKNOWN_IMPLEMENTATION",
"SIMPLE_MODEL",
"SIMPLE_ROUTER",
"RANDOM_ABTEST",
"AVERAGE_COMBINER",
],
type: "string",
},
type: {
enum: [
"UNKNOWN_TYPE",
"ROUTER",
"COMBINER",
"MODEL",
"TRANSFORMER",
"OUTPUT_TRANSFORMER",
],
type: "string",
},
methods: {
type: "array",
items: {
enum: [
"TRANSFORM_INPUT",
"TRANSFORM_OUTPUT",
"ROUTE",
"AGGREGATE",
"SEND_FEEDBACK",
],
type: "string",
},
},
},
},
type: "array",
},
endpoint: {
properties: {
service_host: {
type: "string",
},
service_port: {
type: "integer",
},
type: {
enum: [
"REST",
"GRPC",
],
type: "string",
},
},
},
name: {
type: "string",
},
implementation: {
enum: [
"UNKNOWN_IMPLEMENTATION",
"SIMPLE_MODEL",
"SIMPLE_ROUTER",
"RANDOM_ABTEST",
"AVERAGE_COMBINER",
],
type: "string",
},
type: {
enum: [
"UNKNOWN_TYPE",
"ROUTER",
"COMBINER",
"MODEL",
"TRANSFORMER",
"OUTPUT_TRANSFORMER",
],
type: "string",
},
methods: {
type: "array",
items: {
enum: [
"TRANSFORM_INPUT",
"TRANSFORM_OUTPUT",
"ROUTE",
"AGGREGATE",
"SEND_FEEDBACK",
],
type: "string",
},
},
},
},
type: "array",
},
endpoint: {
properties: {
service_host: {
type: "string",
},
service_port: {
type: "integer",
},
type: {
enum: [
"REST",
"GRPC",
],
type: "string",
},
},
},
name: {
type: "string",
},
implementation: {
enum: [
"UNKNOWN_IMPLEMENTATION",
"SIMPLE_MODEL",
"SIMPLE_ROUTER",
"RANDOM_ABTEST",
"AVERAGE_COMBINER",
],
type: "string",
},
type: {
enum: [
"UNKNOWN_TYPE",
"ROUTER",
"COMBINER",
"MODEL",
"TRANSFORMER",
"OUTPUT_TRANSFORMER",
],
type: "string",
},
methods: {
type: "array",
items: {
enum: [
"TRANSFORM_INPUT",
"TRANSFORM_OUTPUT",
"ROUTE",
"AGGREGATE",
"SEND_FEEDBACK",
],
type: "string",
},
},
},
},
name: {
type: "string",
},
replicas: {
type: "integer",
},
},
},
type: "array",
},
componentSpecs:
{
description: "List of pods belonging to the predictor",
type: "array",
items: podTemplateValidation,
},
},
},
},
},
},
version: "v1alpha2",
},
},
}

View File

@ -0,0 +1,19 @@
The template json files are generated from seldon-core helm charts.
The template_0.2.json is generated using:
```
git clone --branch release-0.2 git@github.com:SeldonIO/seldon-core.git seldon-core-release-0.2
helm template --set ambassador.enabled=true seldon-core-release-0.2/helm-charts/seldon-core > template_0.2.yaml
kubectl convert -f template_0.2.yaml -o json > template_0.2.json
rm template_0.2.yaml
```
The template_0.1.json is generated using:
```
git clone --branch release-0.1 git@github.com:SeldonIO/seldon-core.git seldon-core-release-0.1
helm template --set ambassador.enabled=true seldon-core-release-0.1/helm-charts/seldon-core > template_0.1.yaml
kubectl convert -f template_0.1.yaml -o json > template_0.1.json
rm template_0.1.yaml
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,333 @@
{
"kind": "List",
"apiVersion": "v1",
"metadata": {},
"items": [
{
"kind": "ServiceAccount",
"apiVersion": "v1",
"metadata": {
"name": "seldon",
"creationTimestamp": null
}
},
{
"kind": "RoleBinding",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "seldon",
"creationTimestamp": null
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "seldon",
"namespace": "seldon"
}
],
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "cluster-admin"
}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "seldon-cluster-manager",
"creationTimestamp": null,
"labels": {
"app": "seldon-cluster-manager-server"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "seldon-cluster-manager-server"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "seldon-cluster-manager-server"
}
},
"spec": {
"containers": [
{
"name": "seldon-cluster-manager-container",
"image": "seldonio/cluster-manager:0.1.8",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
}
],
"env": [
{
"name": "JAVA_OPTS"
},
{
"name": "SPRING_OPTS"
},
{
"name": "SELDON_CLUSTER_MANAGER_REDIS_HOST",
"value": "redis"
},
{
"name": "ENGINE_CONTAINER_IMAGE_AND_VERSION",
"value": "seldonio/engine:0.1.8"
},
{
"name": "SELDON_CLUSTER_MANAGER_POD_NAMESPACE",
"valueFrom": {
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
}
}
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"serviceAccountName": "seldon",
"serviceAccount": "seldon",
"securityContext": {},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": 1,
"maxSurge": 1
}
},
"progressDeadlineSeconds": 2147483647
},
"status": {}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "redis",
"creationTimestamp": null,
"labels": {
"app": "redis-app"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "redis-app"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "redis-app"
}
},
"spec": {
"containers": [
{
"name": "redis-container",
"image": "redis:4.0.1",
"ports": [
{
"containerPort": 6379,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"securityContext": {},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": 1,
"maxSurge": 1
}
},
"progressDeadlineSeconds": 2147483647
},
"status": {}
},
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "redis",
"creationTimestamp": null
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 6379,
"targetPort": 6379
}
],
"selector": {
"app": "redis-app"
},
"type": "ClusterIP",
"sessionAffinity": "None"
},
"status": {
"loadBalancer": {}
}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "seldon-apiserver",
"creationTimestamp": null,
"labels": {
"app": "seldon-apiserver-container-app",
"version": "1"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "seldon-apiserver-container-app",
"version": "1"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "seldon-apiserver-container-app",
"version": "1"
},
"annotations": {
"prometheus.io/path": "/prometheus",
"prometheus.io/port": "8080",
"prometheus.io/scrape": "true"
}
},
"spec": {
"containers": [
{
"name": "seldon-apiserver-container",
"image": "seldonio/apife:0.1.8",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
},
{
"containerPort": 5000,
"protocol": "TCP"
}
],
"env": [
{
"name": "SELDON_ENGINE_KAFKA_SERVER",
"value": "kafka:9092"
},
{
"name": "SELDON_CLUSTER_MANAGER_REDIS_HOST",
"value": "redis"
},
{
"name": "SELDON_CLUSTER_MANAGER_POD_NAMESPACE",
"valueFrom": {
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
}
}
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"serviceAccountName": "seldon",
"serviceAccount": "seldon",
"securityContext": {},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": 1,
"maxSurge": 1
}
},
"progressDeadlineSeconds": 2147483647
},
"status": {}
},
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "seldon-apiserver",
"creationTimestamp": null,
"labels": {
"app": "seldon-apiserver-container-app"
}
},
"spec": {
"ports": [
{
"name": "http",
"protocol": "TCP",
"port": 8080,
"targetPort": 8080
},
{
"name": "grpc",
"protocol": "TCP",
"port": 5000,
"targetPort": 5000
}
],
"selector": {
"app": "seldon-apiserver-container-app"
},
"type": "NodePort",
"sessionAffinity": "None",
"externalTrafficPolicy": "Cluster"
},
"status": {
"loadBalancer": {}
}
}
]
}

View File

@ -0,0 +1,775 @@
{
"kind": "List",
"apiVersion": "v1",
"metadata": {},
"items": [
{
"kind": "ServiceAccount",
"apiVersion": "v1",
"metadata": {
"name": "seldon",
"creationTimestamp": null
}
},
{
"kind": "Role",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "seldon-local",
"creationTimestamp": null
},
"rules": [
{
"verbs": [
"*"
],
"apiGroups": [
"*"
],
"resources": [
"deployments",
"services"
]
},
{
"verbs": [
"*"
],
"apiGroups": [
"machinelearning.seldon.io"
],
"resources": [
"*"
]
}
]
},
{
"kind": "ClusterRole",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "seldon-crd",
"creationTimestamp": null
},
"rules": [
{
"verbs": [
"create"
],
"apiGroups": [
"apiextensions.k8s.io"
],
"resources": [
"customresourcedefinitions"
]
}
]
},
{
"kind": "RoleBinding",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "seldon",
"creationTimestamp": null
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "seldon",
"namespace": "seldon"
}
],
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "Role",
"name": "seldon-local"
}
},
{
"kind": "ClusterRoleBinding",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "seldon",
"creationTimestamp": null
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "seldon",
"namespace": "seldon"
}
],
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "seldon-crd"
}
},
{
"kind": "Role",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "ambassador",
"creationTimestamp": null
},
"rules": [
{
"verbs": [
"get",
"list",
"watch"
],
"apiGroups": [
""
],
"resources": [
"services"
]
},
{
"verbs": [
"create",
"update",
"patch",
"get",
"list",
"watch"
],
"apiGroups": [
""
],
"resources": [
"configmaps"
]
},
{
"verbs": [
"get",
"list",
"watch"
],
"apiGroups": [
""
],
"resources": [
"secrets"
]
}
]
},
{
"kind": "RoleBinding",
"apiVersion": "rbac.authorization.k8s.io/v1",
"metadata": {
"name": "ambassador",
"creationTimestamp": null
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "seldon",
"namespace": "seldon"
}
],
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "Role",
"name": "ambassador"
}
},
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "RELEASE-NAME-ambassador",
"creationTimestamp": null,
"labels": {
"service": "ambassador"
},
"annotations": {
"getambassador.io/config": "---\napiVersion: ambassador/v0\nkind: Module\nname: ambassador\nconfig:\n service_port: 8080\n"
}
},
"spec": {
"ports": [
{
"name": "http",
"protocol": "TCP",
"port": 8080,
"targetPort": 8080
}
],
"selector": {
"service": "ambassador"
},
"type": "NodePort",
"sessionAffinity": "None",
"externalTrafficPolicy": "Cluster"
},
"status": {
"loadBalancer": {}
}
},
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "RELEASE-NAME-ambassador-admin",
"creationTimestamp": null,
"labels": {
"service": "ambassador-admin"
}
},
"spec": {
"ports": [
{
"name": "ambassador-admin",
"protocol": "TCP",
"port": 8877,
"targetPort": 8877
}
],
"selector": {
"service": "ambassador"
},
"type": "NodePort",
"sessionAffinity": "None",
"externalTrafficPolicy": "Cluster"
},
"status": {
"loadBalancer": {}
}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "RELEASE-NAME-ambassador",
"creationTimestamp": null,
"labels": {
"service": "ambassador"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"service": "ambassador"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"service": "ambassador"
},
"annotations": {
"sidecar.istio.io/inject": "false"
}
},
"spec": {
"containers": [
{
"name": "ambassador",
"image": "quay.io/datawire/ambassador:0.35.1",
"ports": [
{
"name": "http",
"containerPort": 8080,
"protocol": "TCP"
},
{
"name": "https",
"containerPort": 443,
"protocol": "TCP"
},
{
"name": "admin",
"containerPort": 8877,
"protocol": "TCP"
}
],
"env": [
{
"name": "AMBASSADOR_SINGLE_NAMESPACE",
"value": "true"
},
{
"name": "AMBASSADOR_NAMESPACE",
"valueFrom": {
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
}
}
}
],
"resources": {
"limits": {
"cpu": "1",
"memory": "400Mi"
},
"requests": {
"cpu": "200m",
"memory": "128Mi"
}
},
"livenessProbe": {
"httpGet": {
"path": "/ambassador/v0/check_alive",
"port": "admin",
"scheme": "HTTP"
},
"initialDelaySeconds": 30,
"timeoutSeconds": 1,
"periodSeconds": 3,
"successThreshold": 1,
"failureThreshold": 3
},
"readinessProbe": {
"httpGet": {
"path": "/ambassador/v0/check_ready",
"port": "admin",
"scheme": "HTTP"
},
"initialDelaySeconds": 30,
"timeoutSeconds": 1,
"periodSeconds": 3,
"successThreshold": 1,
"failureThreshold": 3
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent"
},
{
"name": "statsd",
"image": "datawire/prom-statsd-exporter:0.6.0",
"ports": [
{
"name": "metrics",
"containerPort": 9102,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"serviceAccountName": "seldon",
"serviceAccount": "seldon",
"securityContext": {
"runAsUser": 8888
},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": 1,
"maxSurge": 1
}
},
"progressDeadlineSeconds": 2147483647
},
"status": {}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "RELEASE-NAME-seldon-apiserver",
"creationTimestamp": null,
"labels": {
"app": "seldon-apiserver-container-app",
"app.kubernetes.io/component": "seldon-core-apiserver",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "seldon-apiserver-container-app",
"app.kubernetes.io/component": "seldon-core-apiserver",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "seldon-apiserver-container-app",
"app.kubernetes.io/component": "seldon-core-apiserver",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
},
"annotations": {
"prometheus.io/path": "/prometheus",
"prometheus.io/port": "8080",
"prometheus.io/scrape": "true"
}
},
"spec": {
"volumes": [
{
"name": "podinfo",
"downwardAPI": {
"items": [
{
"path": "annotations",
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.annotations"
}
}
],
"defaultMode": 420
}
}
],
"containers": [
{
"name": "seldon-apiserver-container",
"image": "seldonio/apife:0.2.3",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
},
{
"containerPort": 5000,
"protocol": "TCP"
}
],
"env": [
{
"name": "SELDON_ENGINE_KAFKA_SERVER",
"value": "kafka:9092"
},
{
"name": "SELDON_CLUSTER_MANAGER_REDIS_HOST",
"value": "RELEASE-NAME-redis"
},
{
"name": "SELDON_CLUSTER_MANAGER_POD_NAMESPACE",
"valueFrom": {
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
}
}
}
],
"resources": {},
"volumeMounts": [
{
"name": "podinfo",
"mountPath": "/etc/podinfo"
}
],
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent",
"securityContext": {
"runAsUser": 8888,
"procMount": null
}
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"serviceAccountName": "seldon",
"serviceAccount": "seldon",
"securityContext": {},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": "25%",
"maxSurge": "25%"
}
},
"revisionHistoryLimit": 2,
"progressDeadlineSeconds": 600
},
"status": {}
},
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "RELEASE-NAME-seldon-apiserver",
"creationTimestamp": null,
"labels": {
"app": "seldon-apiserver-container-app",
"app.kubernetes.io/component": "seldon-core-apiserver",
"app.kubernetes.io/name": "RELEASE-NAME"
}
},
"spec": {
"ports": [
{
"name": "http",
"protocol": "TCP",
"port": 8080,
"targetPort": 8080
},
{
"name": "grpc",
"protocol": "TCP",
"port": 5000,
"targetPort": 5000
}
],
"selector": {
"app": "seldon-apiserver-container-app"
},
"type": "NodePort",
"sessionAffinity": "None",
"externalTrafficPolicy": "Cluster"
},
"status": {
"loadBalancer": {}
}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "RELEASE-NAME-seldon-cluster-manager",
"creationTimestamp": null,
"labels": {
"app": "seldon-cluster-manager-server",
"app.kubernetes.io/component": "seldon-core-operator",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "seldon-cluster-manager-server",
"app.kubernetes.io/component": "seldon-core-operator",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "seldon-cluster-manager-server",
"app.kubernetes.io/component": "seldon-core-operator",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"spec": {
"containers": [
{
"name": "seldon-cluster-manager-container",
"image": "seldonio/cluster-manager:0.2.3",
"ports": [
{
"containerPort": 8080,
"protocol": "TCP"
}
],
"env": [
{
"name": "JAVA_OPTS"
},
{
"name": "SPRING_OPTS"
},
{
"name": "SELDON_CLUSTER_MANAGER_REDIS_HOST",
"value": "RELEASE-NAME-redis"
},
{
"name": "ENGINE_CONTAINER_IMAGE_AND_VERSION",
"value": "seldonio/engine:0.2.3"
},
{
"name": "ENGINE_CONTAINER_IMAGE_PULL_POLICY",
"value": "IfNotPresent"
},
{
"name": "SELDON_CLUSTER_MANAGER_POD_NAMESPACE",
"valueFrom": {
"fieldRef": {
"apiVersion": "v1",
"fieldPath": "metadata.namespace"
}
}
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent",
"securityContext": {
"runAsUser": 8888,
"procMount": null
}
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 1,
"dnsPolicy": "ClusterFirst",
"serviceAccountName": "seldon",
"serviceAccount": "seldon",
"securityContext": {},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": 1,
"maxSurge": 1
}
},
"progressDeadlineSeconds": 2147483647
},
"status": {}
},
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "RELEASE-NAME-redis",
"creationTimestamp": null,
"labels": {
"app": "RELEASE-NAME-redis-app",
"app.kubernetes.io/component": "seldon-core-redis",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "RELEASE-NAME-redis-app",
"app.kubernetes.io/component": "seldon-core-redis",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "RELEASE-NAME-redis-app",
"app.kubernetes.io/component": "seldon-core-redis",
"app.kubernetes.io/name": "RELEASE-NAME",
"chart": "seldon-core-0.2.3",
"component": "seldon-core",
"heritage": "Tiller",
"release": "RELEASE-NAME"
}
},
"spec": {
"containers": [
{
"name": "redis-container",
"image": "redis:4.0.1",
"ports": [
{
"containerPort": 6379,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": "Always",
"terminationGracePeriodSeconds": 30,
"dnsPolicy": "ClusterFirst",
"securityContext": {},
"schedulerName": "default-scheduler"
}
},
"strategy": {
"type": "RollingUpdate",
"rollingUpdate": {
"maxUnavailable": 1,
"maxSurge": 1
}
},
"progressDeadlineSeconds": 2147483647
},
"status": {}
},
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "RELEASE-NAME-redis",
"creationTimestamp": null,
"labels": {
"app.kubernetes.io/component": "seldon-core-redis",
"app.kubernetes.io/name": "RELEASE-NAME"
}
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 6379,
"targetPort": 6379
}
],
"selector": {
"app": "RELEASE-NAME-redis-app"
},
"type": "ClusterIP",
"sessionAffinity": "None"
},
"status": {
"loadBalancer": {}
}
}
]
}

View File

@ -0,0 +1,35 @@
{
"name": "seldon",
"apiVersion": "0.0.1",
"kind": "ksonnet.io/parts",
"description": "Seldon ML Deployment\n",
"author": "seldon-core team <devext@seldon.io>",
"contributors": [
{
"name": "Clive Cox",
"email": "cc@seldon.io"
}
],
"repository": {
"type": "git",
"url": "https://github.com/kubeflow/kubeflow"
},
"bugs": {
"url": "https://github.com/SeldonIO/seldon-core/issues"
},
"keywords": [
"kubernetes",
"machine learning",
"deployment"
],
"quickStart": {
"prototype": "io.ksonnet.pkg.seldon",
"componentName": "seldon",
"flags": {
"name": "seldon",
"namespace": "default"
},
"comment": "Seldon Core Components."
},
"license": "Apache 2.0"
}

View File

@ -0,0 +1,136 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-abtest-v1alpha1
// @description An AB test between two models for the v1alpha1 CRD (Seldon 0.1.X)
// @shortDescription An AB test between two models
// @param name string Name to give this deployment
// @param imageA string Docker image which contains model A
// @param imageB string Docker image which contains model B
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpointA string REST The endpoint type for modelA : REST or GRPC
// @optionalParam endpointB string REST The endpoint type for modelB: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment =
{
apiVersion: "machinelearning.seldon.io/v1alpha1",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
project_name: params.name,
deployment_version: "v1",
},
name: params.name,
predictors: [
{
componentSpec:
{
spec: {
containers: [
{
image: params.imageA,
imagePullPolicy: "IfNotPresent",
name: "classifier-1",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
{
image: params.imageB,
imagePullPolicy: "IfNotPresent",
name: "classifier-2",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
name: params.name,
replicas: params.replicas,
graph: {
name: "random-ab-test",
endpoint: {},
implementation: "RANDOM_ABTEST",
parameters: [
{
name: "ratioA",
value: "0.5",
type: "FLOAT",
},
],
children: [
{
name: "classifier-1",
endpoint: {
type: params.endpointA,
},
type: "MODEL",
children: [],
},
{
name: "classifier-2",
endpoint: {
type: params.endpointB,
},
type: "MODEL",
children: [],
},
],
},
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,164 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-abtest-v1alpha2
// @description An AB test between two models for the v1alpha2 CRD (Seldon 0.2.X)
// @shortDescription An AB test between two models
// @param name string Name to give this deployment
// @param imageA string Docker image which contains model A
// @param imageB string Docker image which contains model B
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpointA string REST The endpoint type for modelA : REST or GRPC
// @optionalParam endpointB string REST The endpoint type for modelB: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment =
{
apiVersion: "machinelearning.seldon.io/v1alpha2",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
project_name: params.name,
deployment_version: "v1",
},
name: params.name,
predictors: [
{
componentSpecs: [
{
metadata: {
labels: {
version: "v2",
},
},
spec: {
containers: [
{
image: params.imageA,
imagePullPolicy: "IfNotPresent",
name: "classifier-1",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
{
metadata: {
labels: {
version: "v2",
},
},
spec: {
containers: [
{
image: params.imageB,
imagePullPolicy: "IfNotPresent",
name: "classifier-2",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
],
name: params.name,
replicas: params.replicas,
graph: {
name: "random-ab-test",
endpoint: {},
implementation: "RANDOM_ABTEST",
parameters: [
{
name: "ratioA",
value: "0.5",
type: "FLOAT",
},
],
children: [
{
name: "classifier-1",
endpoint: {
type: params.endpointA,
},
type: "MODEL",
children: [],
},
{
name: "classifier-2",
endpoint: {
type: params.endpointB,
},
type: "MODEL",
children: [],
},
],
},
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,86 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon
// @description Seldon Core components. Operator and API FrontEnd.
// @shortDescription Seldon Core components.
// @param name string seldon Name to give seldon
// @optionalParam withRbac string true Whether to include RBAC setup
// @optionalParam withApife string false Whether to include builtin API OAuth gateway server for ingress
// @optionalParam withAmbassador string false Whether to include Ambassador reverse proxy
// @optionalParam apifeServiceType string NodePort API Front End Service Type
// @optionalParam operatorSpringOpts string null cluster manager spring opts
// @optionalParam operatorJavaOpts string null cluster manager java opts
// @optionalParam grpcMaxMessageSize string 4194304 Max gRPC message size
// @optionalParam seldonVersion string 0.2.3 Seldon version
local k = import "k.libsonnet";
local core = import "kubeflow/seldon/core.libsonnet";
local seldonVersion = import "param://seldonVersion";
local name = import "param://name";
local namespace = env.namespace;
local withRbac = import "param://withRbac";
local withApife = import "param://withApife";
local withAmbassador = import "param://withAmbassador";
// APIFE
local apifeImage = "seldonio/apife:" + seldonVersion;
local apifeServiceType = import "param://apifeServiceType";
local grpcMaxMessageSize = import "param://grpcMaxMessageSize";
// Cluster Manager (The CRD Operator)
local operatorImage = "seldonio/cluster-manager:" + seldonVersion;
local operatorSpringOptsParam = import "param://operatorSpringOpts";
local operatorSpringOpts = if operatorSpringOptsParam != "null" then operatorSpringOptsParam else "";
local operatorJavaOptsParam = import "param://operatorJavaOpts";
local operatorJavaOpts = if operatorJavaOptsParam != "null" then operatorJavaOptsParam else "";
// Engine
local engineImage = "seldonio/engine:" + seldonVersion;
// APIFE
local apife = [
core.parts(name, namespace, seldonVersion).apife(apifeImage, withRbac, grpcMaxMessageSize),
core.parts(name, namespace, seldonVersion).apifeService(apifeServiceType),
];
local rbac2 = [
core.parts(name, namespace, seldonVersion).rbacServiceAccount(),
core.parts(name, namespace, seldonVersion).rbacClusterRole(),
core.parts(name, namespace, seldonVersion).rbacRole(),
core.parts(name, namespace, seldonVersion).rbacRoleBinding(),
core.parts(name, namespace, seldonVersion).rbacClusterRoleBinding(),
];
local rbac1 = [
core.parts(name, namespace, seldonVersion).rbacServiceAccount(),
core.parts(name, namespace, seldonVersion).rbacRoleBinding(),
];
local rbac = if std.startsWith(seldonVersion, "0.1") then rbac1 else rbac2;
// Core
local coreComponents = [
core.parts(name, namespace, seldonVersion).deploymentOperator(engineImage, operatorImage, operatorSpringOpts, operatorJavaOpts, withRbac),
core.parts(name, namespace, seldonVersion).redisDeployment(),
core.parts(name, namespace, seldonVersion).redisService(),
core.parts(name, namespace, seldonVersion).crd(),
];
//Ambassador
local ambassadorRbac = [
core.parts(name, namespace, seldonVersion).rbacAmbassadorRole(),
core.parts(name, namespace, seldonVersion).rbacAmbassadorRoleBinding(),
];
local ambassador = [
core.parts(name, namespace, seldonVersion).ambassadorDeployment(),
core.parts(name, namespace, seldonVersion).ambassadorService(),
];
local l1 = if withRbac == "true" then rbac + coreComponents else coreComponents;
local l2 = if withApife == "true" then l1 + apife else l1;
local l3 = if withAmbassador == "true" && withRbac == "true" then l2 + ambassadorRbac else l2;
local l4 = if withAmbassador == "true" then l3 + ambassador else l3;
l4

View File

@ -0,0 +1,151 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-mab-v1alpha1
// @description An e-greeey multi-armed bandit solver between two models for the v1alpha1 CRD (Seldon 0.1.X)
// @shortDescription An e-greeey multi-armed bandit for two models
// @param name string Name to give this deployment
// @param imageA string Docker image which contains model A
// @param imageB string Docker image which contains model
// @optionalParam mabImage string seldonio/mab_epsilon_greedy:1.1 image for multi-armed bandit model
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpointA string REST The endpoint type for modelA : REST or GRPC
// @optionalParam endpointB string REST The endpoint type for modelB: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment =
{
apiVersion: "machinelearning.seldon.io/v1alpha1",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
project_name: params.name,
deployment_version: "v1",
},
name: params.name,
predictors: [
{
componentSpec:
{
spec: {
containers: [
{
image: params.imageA,
imagePullPolicy: "IfNotPresent",
name: "classifier-1",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
{
image: params.imageB,
imagePullPolicy: "IfNotPresent",
name: "classifier-2",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
{
image: params.mabImage,
imagePullPolicy: "IfNotPresent",
name: "eg-router",
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
name: params.name,
replicas: params.replicas,
graph: {
name: "eg-router",
type: "ROUTER",
parameters: [
{
name: "n_branches",
value: "2",
type: "INT",
},
{
name: "epsilon",
value: "0.2",
type: "FLOAT",
},
{
name: "verbose",
value: "1",
type: "BOOL",
},
],
children: [
{
name: "classifier-1",
endpoint: {
type: params.endpointA,
},
type: "MODEL",
children: [],
},
{
name: "classifier-2",
endpoint: {
type: params.endpointB,
},
type: "MODEL",
children: [],
},
],
},
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,191 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-mab-v1alpha2
// @description An e-greeey multi-armed bandit solver between two models for the v1alpha2 CRD (Seldon 0.2.X)
// @shortDescription An e-greeey multi-armed bandit for two models
// @param name string Name to give this deployment
// @param imageA string Docker image which contains model A
// @param imageB string Docker image which contains model
// @optionalParam mabImage string seldonio/mab_epsilon_greedy:1.1 image for multi-armed bandit model
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpointA string REST The endpoint type for modelA : REST or GRPC
// @optionalParam endpointB string REST The endpoint type for modelB: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment =
{
apiVersion: "machinelearning.seldon.io/v1alpha2",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
project_name: params.name,
deployment_version: "v1",
},
name: params.name,
predictors: [
{
componentSpecs: [
{
metadata: {
labels: {
version: "v1",
},
},
spec: {
containers: [
{
image: params.imageA,
imagePullPolicy: "IfNotPresent",
name: "classifier-1",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
{
metadata: {
labels: {
version: "v2",
},
},
spec: {
containers: [
{
image: params.imageB,
imagePullPolicy: "IfNotPresent",
name: "classifier-2",
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
{
spec: {
containers: [
{
image: params.mabImage,
imagePullPolicy: "IfNotPresent",
name: "eg-router",
},
],
terminationGracePeriodSeconds: 1,
},
},
],
name: params.name,
replicas: params.replicas,
graph: {
name: "eg-router",
type: "ROUTER",
parameters: [
{
name: "n_branches",
value: "2",
type: "INT",
},
{
name: "epsilon",
value: "0.2",
type: "FLOAT",
},
{
name: "verbose",
value: "1",
type: "BOOL",
},
],
children: [
{
name: "classifier-1",
endpoint: {
type: params.endpointA,
},
type: "MODEL",
children: [],
},
{
name: "classifier-2",
endpoint: {
type: params.endpointB,
},
type: "MODEL",
children: [],
},
],
},
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,116 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-outlier-detector-v1alpha1
// @description Serve an outlier detector with a single model for the v1alpha1 CRD (Seldon 0.1.X)
// @shortDescription Serve an outlier detector with a model
// @param name string Name to give this deployment
// @param image string Docker image which contains this model
// @optionalParam outlierDetectorImage string seldonio/outlier_mahalanobis:0.3 Docker image for outlier detector
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpoint string REST The endpoint type: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment = {
apiVersion: "machinelearning.seldon.io/v1alpha1",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
deployment_version: "v1",
project_name: params.name,
},
name: params.name,
predictors: [
{
annotations: {
predictor_version: "v1",
},
componentSpec:
{
spec: {
containers: [
{
image: params.image,
imagePullPolicy: "IfNotPresent",
name: params.name,
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
{
image: params.outlierDetectorImage,
imagePullPolicy: "IfNotPresent",
name: "outlier-detector",
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
graph: {
name: "outlier-detector",
type: "TRANSFORMER",
endpoint: {
type: "REST",
},
children: [{
children: [
],
endpoint: {
type: params.endpoint,
},
name: params.name,
type: "MODEL",
}],
},
name: params.name,
replicas: params.replicas,
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,124 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-outlier-detector-v1alpha2
// @description Serve an outlier detector with a single model for the v1alpha2 CRD (Seldon 0.2.X)
// @shortDescription Serve an outlier detector with a model
// @param name string Name to give this deployment
// @param image string Docker image which contains this model
// @optionalParam outlierDetectorImage string seldonio/outlier_mahalanobis:0.3 Docker image for outlier detector
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpoint string REST The endpoint type: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment = {
apiVersion: "machinelearning.seldon.io/v1alpha2",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
deployment_version: "v1",
project_name: params.name,
},
name: params.name,
predictors: [
{
annotations: {
predictor_version: "v1",
},
componentSpecs: [
{
spec: {
containers: [
{
image: params.image,
imagePullPolicy: "IfNotPresent",
name: params.name,
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
{
spec: {
containers: [
{
image: params.outlierDetectorImage,
imagePullPolicy: "IfNotPresent",
name: "outlier-detector",
},
],
terminationGracePeriodSeconds: 1,
},
},
],
graph: {
name: "outlier-detector",
type: "TRANSFORMER",
endpoint: {
type: "REST",
},
children: [{
children: [
],
endpoint: {
type: params.endpoint,
},
name: params.name,
type: "MODEL",
}],
},
name: params.name,
replicas: params.replicas,
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,105 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-serve-simple-v1alpha1
// @description Serve a single seldon model for the v1alpha1 CRD (Seldon 0.1.X)
// @shortDescription Serve a single seldon model
// @param name string Name to give this deployment
// @param image string Docker image which contains this model
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpoint string REST The endpoint type: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment = {
apiVersion: "machinelearning.seldon.io/v1alpha1",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
deployment_version: "v1",
project_name: params.name,
},
name: params.name,
predictors: [
{
annotations: {
predictor_version: "v1",
},
componentSpec: {
spec: {
containers: [
{
image: params.image,
imagePullPolicy: "IfNotPresent",
name: params.name,
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
},
graph: {
children: [
],
endpoint: {
type: params.endpoint,
},
name: params.name,
type: "MODEL",
},
name: params.name,
replicas: params.replicas,
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -0,0 +1,103 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.seldon-serve-simple-v1alpha2
// @description Serve a single seldon model for the v1alpha2 CRD (Seldon 0.2.X)
// @shortDescription Serve a single seldon model
// @param name string Name to give this deployment
// @param image string Docker image which contains this model
// @optionalParam replicas number 1 Number of replicas
// @optionalParam endpoint string REST The endpoint type: REST or GRPC
// @optionalParam pvcName string null Name of PVC
// @optionalParam imagePullSecret string null name of image pull secret
local k = import "k.libsonnet";
local pvcClaim = {
apiVersion: "v1",
kind: "PersistentVolumeClaim",
metadata: {
name: params.pvcName,
},
spec: {
accessModes: [
"ReadWriteOnce",
],
resources: {
requests: {
storage: "10Gi",
},
},
},
};
local seldonDeployment = {
apiVersion: "machinelearning.seldon.io/v1alpha2",
kind: "SeldonDeployment",
metadata: {
labels: {
app: "seldon",
},
name: params.name,
namespace: env.namespace,
},
spec: {
annotations: {
deployment_version: "v1",
project_name: params.name,
},
name: params.name,
predictors: [
{
annotations: {
predictor_version: "v1",
},
componentSpecs: [{
spec: {
containers: [
{
image: params.image,
imagePullPolicy: "IfNotPresent",
name: params.name,
volumeMounts+: if params.pvcName != "null" && params.pvcName != "" then [
{
mountPath: "/mnt",
name: "persistent-storage",
},
] else [],
},
],
terminationGracePeriodSeconds: 1,
imagePullSecrets+: if params.imagePullSecret != "null" && params.imagePullSecret != "" then [
{
name: params.imagePullSecret,
},
] else [],
volumes+: if params.pvcName != "null" && params.pvcName != "" then [
{
name: "persistent-storage",
volumeSource: {
persistentVolumeClaim: {
claimName: params.pvcName,
},
},
},
] else [],
},
}],
graph: {
children: [
],
endpoint: {
type: params.endpoint,
},
name: params.name,
type: "MODEL",
},
name: params.name,
replicas: params.replicas,
},
],
},
};
if params.pvcName == "null" then k.core.v1.list.new([seldonDeployment]) else k.core.v1.list.new([pvcClaim, seldonDeployment])

View File

@ -39,7 +39,7 @@ build-s2i-image:
# Build a serving image in a s2i image, which can be built in the above step.
build-seldon-image:IMG?=gcr.io/kubeflow-examples/xgboost_ames_housing_seldon
build-seldon-image:TAG?=latest
build-seldon-image:SOURCE=../seldon_serve
build-seldon-image:SOURCE?=../seldon_serve
build-seldon-image:
@echo IMG=$(IMG)
@echo GIT_VERSION=$(GIT_VERSION)

View File

@ -0,0 +1,24 @@
import pytest
def pytest_addoption(parser):
parser.addoption(
"--master", action="store", default="", help="IP address of GKE master")
parser.addoption(
"--namespace", action="store", default="", help="namespace of server")
parser.addoption(
"--service", action="store", default="",
help="The name of the mnist K8s service")
@pytest.fixture
def master(request):
return request.config.getoption("--master")
@pytest.fixture
def namespace(request):
return request.config.getoption("--namespace")
@pytest.fixture
def service(request):
return request.config.getoption("--service")

View File

@ -0,0 +1,108 @@
"""Test xgboost_ames_housing.
This file tests that we can send predictions to the model
using REST.
It is an integration test as it depends on having access to
a deployed model.
We use the pytest framework because
1. It can output results in junit format for prow/gubernator
2. It has good support for configuring tests using command line arguments
(https://docs.pytest.org/en/latest/example/simple.html)
Python Path Requirements:
kubeflow/testing/py - https://github.com/kubeflow/testing/tree/master/py
* Provides utilities for testing
Manually running the test
1. Configure your KUBECONFIG file to point to the desired cluster
"""
import json
import logging
import os
import subprocess
import requests
from retrying import retry
import six
from kubernetes.config import kube_config
from kubernetes import client as k8s_client
import pytest
from kubeflow.testing import util
@retry(wait_exponential_multiplier=10000, wait_exponential_max=100000,
stop_max_delay=10*60*1000)
def send_request(*args, **kwargs):
# We don't use util.run because that ends up including the access token
# in the logs
token = subprocess.check_output(["gcloud", "auth", "print-access-token"])
if six.PY3 and hasattr(token, "decode"):
token = token.decode()
token = token.strip()
headers = {
"Authorization": "Bearer " + token,
}
if "headers" not in kwargs:
kwargs["headers"] = {}
kwargs["headers"].update(headers)
r = requests.post(*args, **kwargs)
if r.status_code != requests.codes.OK:
msg = "Request to {0} exited with status code: {1} and content: {2}".format(
*args, r.status_code, r.content)
logging.error(msg)
raise RuntimeError(msg)
return r
def test_predict(master, namespace, service):
app_credentials = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
if app_credentials:
print("Activate service account")
util.run(["gcloud", "auth", "activate-service-account",
"--key-file=" + app_credentials])
if not master:
print("--master set; using kubeconfig")
# util.load_kube_config appears to hang on python3
kube_config.load_kube_config()
api_client = k8s_client.ApiClient()
host = api_client.configuration.host
print("host={0}".format(host))
master = host.rsplit("/", 1)[-1]
this_dir = os.path.dirname(__file__)
test_data = os.path.join(this_dir, "query.json")
with open(test_data) as hf:
instances = json.load(hf)
# We proxy the request through the APIServer so that we can connect
# from outside the cluster.
url = ("https://{master}/api/v1/namespaces/{namespace}/services/{service}:8000"
"/proxy/api/v0.1/predictions").format(
master=master, namespace=namespace, service=service)
logging.info("Request: %s", url)
r = send_request(url, json=instances, verify=False)
content = r.content
if six.PY3 and hasattr(content, "decode"):
content = content.decode()
result = json.loads(content)
assert result["data"]["tensor"]["values"] == [97522.359375, 97522.359375]
logging.info("URL %s returned; %s", url, content)
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)
pytest.main()

View File

@ -0,0 +1,49 @@
{
"data": {
"tensor": {
"shape": [
1,
37
],
"values": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37
]
}
}
}