Merge "services/cluster: Add vkube command."
diff --git a/services/cluster/vkube/cluster-agent.go b/services/cluster/vkube/cluster-agent.go
new file mode 100644
index 0000000..20f267d
--- /dev/null
+++ b/services/cluster/vkube/cluster-agent.go
@@ -0,0 +1,220 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/services/device"
+)
+
+const (
+	clusterAgentServiceName     = "cluster-agent"
+	clusterAgentServicePort     = 8193
+	clusterAgentApplicationName = "cluster-agentd"
+)
+
+// createClusterAgent creates a ReplicationController and a Service to run the
+// cluster agent.
+func createClusterAgent(ctx *context.T, config *vkubeConfig) error {
+	if err := createNamespaceIfNotExist(config.ClusterAgent.Namespace); err != nil {
+		return err
+	}
+	version := "latest"
+	if p := strings.Split(config.ClusterAgent.Image, ":"); len(p) == 2 {
+		version = p[1]
+	}
+	ca := object{
+		"apiVersion": "v1",
+		"kind":       "ReplicationController",
+		"metadata": object{
+			"name": clusterAgentApplicationName + "-" + version,
+			"labels": object{
+				"application": clusterAgentApplicationName,
+			},
+			"namespace": config.ClusterAgent.Namespace,
+		},
+		"spec": object{
+			"replicas": 1,
+			"template": object{
+				"metadata": object{
+					"labels": object{
+						"application": clusterAgentApplicationName,
+						"deployment":  version,
+					},
+				},
+				"spec": object{
+					"containers": []object{
+						object{
+							"name":  "cluster-agentd",
+							"image": config.ClusterAgent.Image,
+							"ports": []object{
+								object{
+									"containerPort": clusterAgentServicePort,
+								},
+							},
+							"resources": object{
+								"limits": object{
+									"cpu":    config.ClusterAgent.CPU,
+									"memory": config.ClusterAgent.Memory,
+								},
+							},
+							"volumeMounts": []object{
+								object{
+									"name":      "data",
+									"mountPath": "/data",
+								},
+								object{
+									"name":      "logs",
+									"mountPath": "/logs",
+								},
+							},
+							"env": []object{
+								object{
+									"name":  "ROOT_BLESSINGS",
+									"value": rootBlessings(ctx),
+								},
+								object{
+									"name":  "CLAIMER",
+									"value": clusterAgentClaimer(config),
+								},
+								object{
+									"name":  "ADMIN",
+									"value": config.ClusterAgent.Admin,
+								},
+								object{
+									"name":  "DATADIR",
+									"value": "/data",
+								},
+								object{
+									"name":  "LOGDIR",
+									"value": "/logs",
+								},
+							},
+						},
+					},
+					"volumes": []interface{}{
+						object{
+							"name":     "logs",
+							"emptyDir": object{},
+						},
+					},
+				},
+			},
+		},
+	}
+	if config.ClusterAgent.PersistentDisk == "" {
+		ca.append("spec.template.spec.volumes", object{
+			"name":     "data",
+			"emptyDir": object{},
+		})
+	} else {
+		ca.append("spec.template.spec.volumes", object{
+			"name": "data",
+			"gcePersistentDisk": object{
+				"pdName": config.ClusterAgent.PersistentDisk,
+				"fsType": "ext4",
+			},
+		})
+	}
+
+	if out, err := kubectlCreate(ca); err != nil {
+		return fmt.Errorf("failed to create replication controller: %v\n%s\n", err, string(out))
+	}
+
+	svc := object{
+		"apiVersion": "v1",
+		"kind":       "Service",
+		"metadata": object{
+			"name":      clusterAgentServiceName,
+			"namespace": config.ClusterAgent.Namespace,
+		},
+		"spec": object{
+			"ports": []object{
+				object{
+					"port":       clusterAgentServicePort,
+					"targetPort": clusterAgentServicePort,
+				},
+			},
+			"selector": object{
+				"application": clusterAgentApplicationName,
+			},
+			"type": "LoadBalancer",
+		},
+	}
+	if config.ClusterAgent.ExternalIP != "" {
+		svc.set("spec.loadBalancerIP", config.ClusterAgent.ExternalIP)
+	}
+	if out, err := kubectlCreate(svc); err != nil {
+		return fmt.Errorf("failed to create service: %v\n%s\n", err, string(out))
+	}
+	return nil
+}
+
+// stopClusterAgent stops the cluster agent ReplicationController and deletes
+// its Service.
+func stopClusterAgent(config *vkubeConfig) error {
+	if out, err := kubectl("--namespace="+config.ClusterAgent.Namespace, "stop", "rc", "-l", "application="+clusterAgentApplicationName); err != nil {
+		return fmt.Errorf("failed to stop %s: %v: %s", clusterAgentApplicationName, err, out)
+	}
+	if out, err := kubectl("--namespace="+config.ClusterAgent.Namespace, "delete", "service", clusterAgentServiceName); err != nil {
+		return fmt.Errorf("failed to delete %s: %v: %s", clusterAgentServiceName, err, out)
+	}
+	return nil
+}
+
+// clusterAgentClaimer returns the blessing name of the claimer of the cluster
+// agent.
+func clusterAgentClaimer(config *vkubeConfig) string {
+	p := strings.Split(config.ClusterAgent.Blessing, security.ChainSeparator)
+	return strings.Join(p[:len(p)-1], security.ChainSeparator)
+}
+
+// findClusterAgent returns the external address of the cluster agent.
+func findClusterAgent(config *vkubeConfig, includeBlessings bool) (string, error) {
+	out, err := kubectl("--namespace="+config.ClusterAgent.Namespace, "get", "service", clusterAgentServiceName, "-o", "json")
+	if err != nil {
+		return "", fmt.Errorf("failed to get info of %s: %v: %s", clusterAgentServiceName, err, out)
+	}
+	var svc object
+	if err := svc.importJSON(out); err != nil {
+		return "", fmt.Errorf("failed to parse kubectl output: %v", err)
+	}
+	ports := svc.getObjectArray("spec.ports")
+	if len(ports) == 0 {
+		return "", fmt.Errorf("service %q has no ports", clusterAgentServiceName)
+	}
+	port := ports[0].getInt("port")
+	if port < 0 {
+		return "", fmt.Errorf("service %q has no valid port: %v", clusterAgentServiceName, port)
+	}
+	ingress := svc.getObjectArray("status.loadBalancer.ingress")
+	if len(ingress) == 0 {
+		return "", fmt.Errorf("service %q has no loadbalancer ingress", clusterAgentServiceName)
+	}
+	ip := ingress[0].getString("ip")
+	if ip == "" {
+		return "", fmt.Errorf("service %q loadbalancer has no valid ip", clusterAgentServiceName)
+	}
+	if includeBlessings {
+		return fmt.Sprintf("/(%s)@%s:%d", config.ClusterAgent.Blessing, ip, port), nil
+	}
+	return fmt.Sprintf("/%s:%d", ip, port), nil
+}
+
+// claimClusterAgent claims the cluster agent with the given blessing extension.
+func claimClusterAgent(ctx *context.T, config *vkubeConfig, extension string) error {
+	addr, err := findClusterAgent(config, false)
+	if err != nil {
+		return err
+	}
+	if err := device.ClaimableClient(addr).Claim(ctx, "", &granter{extension: extension}); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/services/cluster/vkube/config.go b/services/cluster/vkube/config.go
new file mode 100644
index 0000000..a7c2c55
--- /dev/null
+++ b/services/cluster/vkube/config.go
@@ -0,0 +1,72 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+)
+
+// The config file used by the vkube command.
+type vkubeConfig struct {
+	// The GCE project name.
+	Project string `json:"project"`
+	// The GCE zone.
+	Zone string `json:"zone"`
+	// The name of the Kubernetes cluster.
+	Cluster string `json:"cluster"`
+
+	ClusterAgent clusterAgentConfig `json:"clusterAgent"`
+	PodAgent     podAgentConfig     `json:"podAgent"`
+}
+
+type clusterAgentConfig struct {
+	// The Kubernetes namespace of the cluster agent. An empty
+	// value is equivalent to "default".
+	Namespace string `json:"namespace"`
+	// The name of the docker image for the cluster agent.
+	Image string `json:"image"`
+	// The amount of CPU to reserve for the cluster agent.
+	CPU string `json:"cpu"`
+	// The amount of memory to reserve for the cluster agent.
+	Memory string `json:"memory"`
+	// The blessing name of the cluster agent.
+	Blessing string `json:"blessing"`
+	// The blessing pattern of the cluster agent admin, i.e. who's
+	// allowed to create and delete secrets.
+	Admin string `json:"admin"`
+	// The external IP address of the cluster agent. An empty value
+	// means that an ephemeral address will be used.
+	// TODO(rthellend): This doesn't currently work.
+	// https://github.com/kubernetes/kubernetes/issues/10323
+	// https://github.com/kubernetes/kubernetes/pull/13005
+	ExternalIP string `json:"externalIP"`
+	// The name of the Persistent Disk of the cluster agent. An
+	// value means that the cluster agent won't use a persistent
+	// disk.
+	PersistentDisk string `json:"persistentDisk"`
+}
+
+type podAgentConfig struct {
+	// The name of the docker image for the pod agent.
+	Image string `json:"image"`
+}
+
+// readConfig reads a config file.
+func readConfig(fileName string) (*vkubeConfig, error) {
+	data, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		return nil, err
+	}
+	var config vkubeConfig
+	if err := json.Unmarshal(data, &config); err != nil {
+		return nil, fmt.Errorf("json.Unmarshal: %v", err)
+	}
+	if config.ClusterAgent.Namespace == "" {
+		config.ClusterAgent.Namespace = "default"
+	}
+	return &config, nil
+}
diff --git a/services/cluster/vkube/doc.go b/services/cluster/vkube/doc.go
new file mode 100644
index 0000000..24bbfaf
--- /dev/null
+++ b/services/cluster/vkube/doc.go
@@ -0,0 +1,179 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+Manages Vanadium applications on kubernetes
+
+Usage:
+   vkube [flags] <command>
+
+The vkube commands are:
+   get-credentials     Gets the kubernetes credentials from Google Cloud.
+   start               Starts an application.
+   update              Updates an application.
+   stop                Stops an application.
+   start-cluster-agent Starts the cluster agent.
+   stop-cluster-agent  Stops the cluster agent.
+   claim-cluster-agent Claims the cluster agent.
+   build-docker-images Builds the docker images for the cluster and pod agents.
+   help                Display help for commands or topics
+
+The vkube flags are:
+ -config=vkube.cfg
+   The 'vkube.cfg' file to use.
+ -gcloud=gcloud
+   The 'gcloud' binary to use.
+ -kubectl=kubectl
+   The 'kubectl' binary to use.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -metadata=<just specify -metadata to activate>
+   Displays metadata for the program and exits.
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -time=false
+   Dump timing information to stderr before exiting the program.
+ -v=0
+   log level for V logs
+ -v23.credentials=
+   directory to use for storing security credentials
+ -v23.i18n-catalogue=
+   18n catalogue files to load, comma separated
+ -v23.namespace.root=[/(dev.v.io/role/vprod/service/mounttabled)@ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -v23.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -v23.tcp.address=
+   address to listen on
+ -v23.tcp.protocol=wsh
+   protocol to listen with
+ -v23.vtrace.cache-size=1024
+   The number of vtrace traces to store in memory.
+ -v23.vtrace.collect-regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -v23.vtrace.dump-on-shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -v23.vtrace.sample-rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for filename-filtered logging
+ -vpath=
+   comma-separated list of pattern=N settings for file pathname-filtered logging
+
+Vkube get-credentials
+
+Gets the kubernetes credentials from Google Cloud.
+
+Usage:
+   vkube get-credentials
+
+Vkube start
+
+Starts an application.
+
+Usage:
+   vkube start [flags] <extension>
+
+<extension> The blessing name extension to give to the application.
+
+The vkube start flags are:
+ -f=
+   Filename to use to create the kubernetes resource.
+
+Vkube update
+
+Updates an application to a new version with a rolling update, preserving the
+existing blessings.
+
+Usage:
+   vkube update [flags]
+
+The vkube update flags are:
+ -f=
+   Filename to use to update the kubernetes resource.
+
+Vkube stop
+
+Stops an application.
+
+Usage:
+   vkube stop [flags]
+
+The vkube stop flags are:
+ -f=
+   Filename to use to stop the kubernetes resource.
+
+Vkube start-cluster-agent
+
+Starts the cluster agent.
+
+Usage:
+   vkube start-cluster-agent
+
+Vkube stop-cluster-agent
+
+Stops the cluster agent.
+
+Usage:
+   vkube stop-cluster-agent
+
+Vkube claim-cluster-agent
+
+Claims the cluster agent.
+
+Usage:
+   vkube claim-cluster-agent
+
+Vkube build-docker-images
+
+Builds the docker images for the cluster and pod agents.
+
+Usage:
+   vkube build-docker-images [flags]
+
+The vkube build-docker-images flags are:
+ -v=false
+   When true, the output is more verbose.
+
+Vkube help - Display help for commands or topics
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+   vkube help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The vkube help flags are:
+ -style=compact
+   The formatting style for help output:
+      compact - Good for compact cmdline output.
+      full    - Good for cmdline output, shows all global flags.
+      godoc   - Good for godoc processing.
+   Override the default by setting the CMDLINE_STYLE environment variable.
+ -width=<terminal width>
+   Format output to this target width in runes, or unlimited if width < 0.
+   Defaults to the terminal width if available.  Override the default by setting
+   the CMDLINE_WIDTH environment variable.
+*/
+package main
diff --git a/services/cluster/vkube/docker.go b/services/cluster/vkube/docker.go
new file mode 100644
index 0000000..4eb223b
--- /dev/null
+++ b/services/cluster/vkube/docker.go
@@ -0,0 +1,161 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+const (
+	clusterAgentDockerfile = `
+FROM debian:stable
+
+# gcloud
+RUN apt-get update && apt-get install -y -qq --no-install-recommends wget unzip python php5-mysql php5-cli php5-cgi openjdk-7-jre-headless openssh-client python-openssl && apt-get clean
+RUN wget https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.zip && unzip google-cloud-sdk.zip && rm google-cloud-sdk.zip
+ENV CLOUDSDK_PYTHON_SITEPACKAGES 1
+ENV HOME /root
+RUN google-cloud-sdk/install.sh --usage-reporting=false --path-update=true --bash-completion=true --rc-path=/root/.bashrc --disable-installation-options && \
+  google-cloud-sdk/bin/gcloud --quiet components update preview alpha beta app kubectl && \
+  google-cloud-sdk/bin/gcloud --quiet config set component_manager/disable_update_check true
+ENV PATH /google-cloud-sdk/bin:$PATH
+
+# vanadium
+#RUN apt-get install --no-install-recommends -y -q libssl1.0.0
+ADD claimable cluster_agent cluster_agentd init.sh /usr/local/bin/
+RUN chmod 755 /usr/local/bin/*
+
+EXPOSE 8193
+CMD ["/usr/local/bin/init.sh"]
+`
+	clusterAgentInitSh = `#!/bin/sh
+if [ ! -e "${DATADIR}/perms" ]; then
+  # Not claimed
+  /usr/local/bin/claimable \
+    --v23.credentials="${DATADIR}/creds" \
+    --v23.tcp.address=:8193 \
+    --root-blessings="${ROOT_BLESSINGS}" \
+    --perms-dir="${DATADIR}/perms" \
+    --v23.permissions.literal="{\"Admin\":{\"In\":[\"${CLAIMER}\"]}}" \
+    --log_dir="${LOGDIR}" \
+    --alsologtostderr=false
+fi
+
+mkdir -p "${DATADIR}/blessings"
+
+exec /usr/local/bin/cluster_agentd \
+  --v23.credentials="${DATADIR}/creds" \
+  --v23.tcp.address=:8193 \
+  --v23.permissions.literal="{\"Admin\":{\"In\":[\"${ADMIN}\"]}}" \
+  --log_dir="${LOGDIR}" \
+  --root-dir="${DATADIR}/blessings" \
+  --alsologtostderr=false
+`
+
+	podAgentDockerfile = `
+FROM debian:stable
+RUN apt-get update && apt-get install --no-install-recommends -y -q libssl1.0.0
+ADD pod_agentd /usr/local/bin/
+RUN chmod 755 /usr/local/bin/pod_agentd
+`
+)
+
+type dockerFile struct {
+	name    string
+	content []byte
+}
+
+type dockerCmd struct {
+	name string
+	args []string
+}
+
+func buildDockerImages(config *vkubeConfig, verbose bool, stdout io.Writer) error {
+	ts := time.Now().Format("20060102150405")
+	// Cluster agent image.
+	imageName := removeTag(config.ClusterAgent.Image)
+	imageNameTag := fmt.Sprintf("%s:%s", imageName, ts)
+
+	var out io.Writer
+	if verbose {
+		out = stdout
+	}
+
+	if err := buildDockerImage([]dockerFile{
+		{"Dockerfile", []byte(clusterAgentDockerfile)},
+		{"init.sh", []byte(clusterAgentInitSh)},
+	}, []dockerCmd{
+		{"jiri", []string{"go", "build", "-o", "claimable", "v.io/x/ref/services/device/claimable"}},
+		{"jiri", []string{"go", "build", "-o", "cluster_agent", "v.io/x/ref/services/cluster/cluster_agent"}},
+		{"jiri", []string{"go", "build", "-o", "cluster_agentd", "v.io/x/ref/services/cluster/cluster_agentd"}},
+		{"docker", []string{"build", "-t", imageName, "."}},
+		{"docker", []string{"tag", imageName, imageNameTag}},
+		{flagGcloudBin, []string{"--project=" + config.Project, "docker", "push", imageName}},
+	}, out); err != nil {
+		return err
+	}
+	fmt.Fprintf(stdout, "Pushed %s successfully.\n", imageNameTag)
+
+	// Pod agent image.
+	imageName = removeTag(config.PodAgent.Image)
+	imageNameTag = fmt.Sprintf("%s:%s", imageName, ts)
+
+	if err := buildDockerImage([]dockerFile{
+		{"Dockerfile", []byte(podAgentDockerfile)},
+	}, []dockerCmd{
+		{"jiri", []string{"go", "build", "-o", "pod_agentd", "v.io/x/ref/services/agent/pod_agentd"}},
+		{"docker", []string{"build", "-t", imageName, "."}},
+		{"docker", []string{"tag", imageName, imageNameTag}},
+		{flagGcloudBin, []string{"--project=" + config.Project, "docker", "push", imageName}},
+	}, out); err != nil {
+		return err
+	}
+	fmt.Fprintf(stdout, "Pushed %s successfully.\n", imageNameTag)
+	return nil
+}
+
+func removeTag(name string) string {
+	if p := strings.Split(name, ":"); len(p) > 0 {
+		return p[0]
+	}
+	return ""
+}
+
+func buildDockerImage(files []dockerFile, cmds []dockerCmd, stdout io.Writer) error {
+	workDir, err := ioutil.TempDir("", "docker-build-")
+	if err != nil {
+		return err
+	}
+	defer os.RemoveAll(workDir)
+
+	for _, f := range files {
+		if stdout != nil {
+			fmt.Fprintf(stdout, "#### Writing %q\n", f.name)
+		}
+		if err := ioutil.WriteFile(filepath.Join(workDir, f.name), f.content, 0600); err != nil {
+			return fmt.Errorf("failed to write %q: %v", f.name, err)
+		}
+	}
+	for _, c := range cmds {
+		if stdout != nil {
+			fmt.Fprintf(stdout, "#### Running %s %s\n", c.name, strings.Join(c.args, " "))
+		}
+		cmd := exec.Command(c.name, c.args...)
+		cmd.Dir = workDir
+		cmd.Stdout = stdout
+		cmd.Stderr = stdout
+		if err := cmd.Run(); err != nil {
+			return fmt.Errorf("%v failed: %v", c, err)
+		}
+	}
+	return nil
+}
diff --git a/services/cluster/vkube/main.go b/services/cluster/vkube/main.go
new file mode 100644
index 0000000..4360be1
--- /dev/null
+++ b/services/cluster/vkube/main.go
@@ -0,0 +1,291 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/verror"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+var (
+	flagConfigFile   string
+	flagKubectlBin   string
+	flagGcloudBin    string
+	flagResourceFile string
+	flagVerbose      bool
+)
+
+func main() {
+	cmdline.HideGlobalFlagsExcept()
+
+	cmd := &cmdline.Command{
+		Name:  "vkube",
+		Short: "Manages Vanadium applications on kubernetes",
+		Long:  "Manages Vanadium applications on kubernetes",
+		Children: []*cmdline.Command{
+			cmdGetCredentials,
+			cmdStart,
+			cmdUpdate,
+			cmdStop,
+			cmdStartClusterAgent,
+			cmdStopClusterAgent,
+			cmdClaimClusterAgent,
+			cmdBuildDockerImages,
+		},
+	}
+	cmd.Flags.StringVar(&flagConfigFile, "config", "vkube.cfg", "The 'vkube.cfg' file to use.")
+	cmd.Flags.StringVar(&flagKubectlBin, "kubectl", "kubectl", "The 'kubectl' binary to use.")
+	cmd.Flags.StringVar(&flagGcloudBin, "gcloud", "gcloud", "The 'gcloud' binary to use.")
+
+	cmdStart.Flags.StringVar(&flagResourceFile, "f", "", "Filename to use to create the kubernetes resource.")
+
+	cmdUpdate.Flags.StringVar(&flagResourceFile, "f", "", "Filename to use to update the kubernetes resource.")
+
+	cmdStop.Flags.StringVar(&flagResourceFile, "f", "", "Filename to use to stop the kubernetes resource.")
+
+	cmdBuildDockerImages.Flags.BoolVar(&flagVerbose, "v", false, "When true, the output is more verbose.")
+
+	cmdline.Main(cmd)
+}
+
+var cmdGetCredentials = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdGetCredentials),
+	Name:   "get-credentials",
+	Short:  "Gets the kubernetes credentials from Google Cloud.",
+	Long:   "Gets the kubernetes credentials from Google Cloud.",
+}
+
+func runCmdGetCredentials(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	if config.Cluster == "" {
+		return fmt.Errorf("Cluster must be set.")
+	}
+	if config.Project == "" {
+		return fmt.Errorf("Project must be set.")
+	}
+	if config.Zone == "" {
+		return fmt.Errorf("Zone must be set.")
+	}
+	return getCredentials(config.Cluster, config.Project, config.Zone)
+}
+
+var cmdStart = &cmdline.Command{
+	Runner:   v23cmd.RunnerFunc(runCmdStart),
+	Name:     "start",
+	Short:    "Starts an application.",
+	Long:     "Starts an application.",
+	ArgsName: "<extension>",
+	ArgsLong: "<extension> The blessing name extension to give to the application.",
+}
+
+func runCmdStart(ctx *context.T, env *cmdline.Env, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return env.UsageErrorf("start: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	extension := args[0]
+
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	if flagResourceFile == "" {
+		return fmt.Errorf("-f must be specified.")
+	}
+	rc, err := readReplicationControllerConfig(flagResourceFile)
+	if err != nil {
+		return err
+	}
+	for _, v := range []string{"spec.template.metadata.labels.application", "spec.template.metadata.labels.deployment"} {
+		if rc.getString(v) == "" {
+			fmt.Fprintf(env.Stderr, "WARNING: %q is not set. Rolling updates will not work.\n", v)
+		}
+	}
+	agentAddr, err := findClusterAgent(config, true)
+	if err != nil {
+		return err
+	}
+	secretName, err := makeSecretName()
+	if err != nil {
+		return err
+	}
+	namespace := rc.getString("metadata.namespace")
+	appName := rc.getString("spec.template.metadata.labels.application")
+	if n, err := findReplicationControllerNameForApp(appName, namespace); err == nil {
+		return fmt.Errorf("replication controller for application=%q already running: %s", appName, n)
+	}
+	if err := createSecret(ctx, secretName, namespace, agentAddr, extension); err != nil {
+		return err
+	}
+	fmt.Fprintln(env.Stdout, "Created Secret successfully.")
+
+	if err := createReplicationController(ctx, config, rc, secretName); err != nil {
+		if err := deleteSecret(ctx, config, secretName, namespace); err != nil {
+			ctx.Error(err)
+		}
+		return err
+	}
+	fmt.Fprintln(env.Stdout, "Created replication controller successfully.")
+	return nil
+}
+
+var cmdUpdate = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdUpdate),
+	Name:   "update",
+	Short:  "Updates an application.",
+	Long:   "Updates an application to a new version with a rolling update, preserving the existing blessings.",
+}
+
+func runCmdUpdate(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	if flagResourceFile == "" {
+		return fmt.Errorf("-f must be specified.")
+	}
+	rc, err := readReplicationControllerConfig(flagResourceFile)
+	if err != nil {
+		return err
+	}
+	if err := updateReplicationController(ctx, config, rc); err != nil {
+		return err
+	}
+	fmt.Fprintln(env.Stdout, "Updated replication controller successfully.")
+	return nil
+}
+
+var cmdStop = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdStop),
+	Name:   "stop",
+	Short:  "Stops an application.",
+	Long:   "Stops an application.",
+}
+
+func runCmdStop(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	if flagResourceFile == "" {
+		return fmt.Errorf("-f must be specified.")
+	}
+	rc, err := readReplicationControllerConfig(flagResourceFile)
+	if err != nil {
+		return err
+	}
+	name := rc.getString("metadata.name")
+	if name == "" {
+		return fmt.Errorf("metadata.name must be set")
+	}
+	namespace := rc.getString("metadata.namespace")
+	secretName, err := findSecretName(name, namespace)
+	if err != nil {
+		return err
+	}
+	if out, err := kubectl("--namespace="+namespace, "stop", "rc", name); err != nil {
+		return fmt.Errorf("failed to stop replication controller: %v: %s", err, out)
+	}
+	fmt.Fprintf(env.Stdout, "Stopping replication controller.\n")
+	if err := deleteSecret(ctx, config, secretName, namespace); err != nil {
+		return fmt.Errorf("failed to delete Secret: %v", err)
+	}
+	fmt.Fprintf(env.Stdout, "Deleting Secret.\n")
+	return nil
+}
+
+var cmdStartClusterAgent = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdStartClusterAgent),
+	Name:   "start-cluster-agent",
+	Short:  "Starts the cluster agent.",
+	Long:   "Starts the cluster agent.",
+}
+
+func runCmdStartClusterAgent(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	if err := createClusterAgent(ctx, config); err != nil {
+		return err
+	}
+	fmt.Fprintf(env.Stdout, "Starting Cluster Agent.\n")
+	return nil
+}
+
+var cmdStopClusterAgent = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdStopClusterAgent),
+	Name:   "stop-cluster-agent",
+	Short:  "Stops the cluster agent.",
+	Long:   "Stops the cluster agent.",
+}
+
+func runCmdStopClusterAgent(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	if err := stopClusterAgent(config); err != nil {
+		return err
+	}
+	fmt.Fprintf(env.Stdout, "Stopping Cluster Agent.\n")
+	return nil
+}
+
+var cmdClaimClusterAgent = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdClaimClusterAgent),
+	Name:   "claim-cluster-agent",
+	Short:  "Claims the cluster agent.",
+	Long:   "Claims the cluster agent.",
+}
+
+func runCmdClaimClusterAgent(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	myBlessings := v23.GetPrincipal(ctx).BlessingStore().Default()
+	claimer := clusterAgentClaimer(config)
+	if !myBlessings.CouldHaveNames([]string{claimer}) {
+		return fmt.Errorf("principal isn't the expected claimer: got %q, expected %q", myBlessings, claimer)
+	}
+	extension := strings.TrimPrefix(config.ClusterAgent.Blessing, claimer+security.ChainSeparator)
+	if err := claimClusterAgent(ctx, config, extension); err != nil {
+		if verror.ErrorID(err) == verror.ErrUnknownMethod.ID {
+			return fmt.Errorf("already claimed")
+		}
+		return err
+	}
+	fmt.Fprintf(env.Stdout, "Claimed Cluster Agent successfully.\n")
+	return nil
+}
+
+var cmdBuildDockerImages = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runCmdBuildDockerImages),
+	Name:   "build-docker-images",
+	Short:  "Builds the docker images for the cluster and pod agents.",
+	Long:   "Builds the docker images for the cluster and pod agents.",
+}
+
+func runCmdBuildDockerImages(ctx *context.T, env *cmdline.Env, args []string) error {
+	config, err := readConfig(flagConfigFile)
+	if err != nil {
+		return err
+	}
+	return buildDockerImages(config, flagVerbose, env.Stdout)
+}
diff --git a/services/cluster/vkube/object.go b/services/cluster/vkube/object.go
new file mode 100644
index 0000000..4d517e0
--- /dev/null
+++ b/services/cluster/vkube/object.go
@@ -0,0 +1,149 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+)
+
+// object simplifies the parsing and handling of json objects that are
+// unmarshaled into an empty interface.
+type object map[string]interface{}
+
+func (o *object) importJSON(data []byte) error {
+	var decode interface{}
+	if err := json.Unmarshal(data, &decode); err != nil {
+		return err
+	}
+	c := convertToObject(decode)
+	var ok bool
+	if *o, ok = c.(object); !ok {
+		return fmt.Errorf("object is %T", c)
+	}
+	return nil
+}
+
+// convertToObject converts all occurrences of map[string]interface{} to object.
+func convertToObject(i interface{}) interface{} {
+	switch obj := i.(type) {
+	case map[string]interface{}:
+		for k, v := range obj {
+			obj[k] = convertToObject(v)
+		}
+		return object(obj)
+	case []interface{}:
+		for x, y := range obj {
+			obj[x] = convertToObject(y)
+		}
+		return obj
+	default:
+		return obj
+	}
+}
+
+func (o object) json() ([]byte, error) {
+	return json.MarshalIndent(o, "", "  ")
+}
+
+// get retrieves the value of an object inside this object, e.g.:
+// if o = { "a": { "b": "c" } }, o.get("a.b") == "c".
+func (o object) get(name string) interface{} {
+	parts := strings.Split(name, ".")
+	var obj interface{} = o
+	for _, p := range parts {
+		m, ok := obj.(object)
+		if !ok {
+			return nil
+		}
+		var exists bool
+		if obj, exists = m[p]; !exists {
+			return nil
+		}
+	}
+	return obj
+}
+
+// set sets the value of an object inside this object, e.g.:
+// if o = { "a": { "b": "c" } }, o.set("a.b", "X") change "c" to "X".
+func (o object) set(name string, value interface{}) error {
+	parts := strings.Split(name, ".")
+	var obj interface{} = o
+	for {
+		m, ok := obj.(object)
+		if !ok {
+			return fmt.Errorf("%q not an object", name)
+		}
+
+		p := parts[0]
+		parts = parts[1:]
+
+		if len(parts) == 0 {
+			m[p] = value
+			break
+		}
+		if obj, ok = m[p]; !ok {
+			obj = make(object)
+			m[p] = obj
+		}
+	}
+	return nil
+}
+
+// getString retrieves a string object.
+func (c object) getString(name string) string {
+	switch s := c.get(name).(type) {
+	case string:
+		return s
+	case nil:
+		return ""
+	default:
+		return fmt.Sprintf("%v", s)
+	}
+}
+
+// getString retrieves a integer object.
+func (c object) getInt(name string) int {
+	switch v := c.get(name).(type) {
+	case int:
+		return v
+	case float64:
+		return int(v)
+	default:
+		return -1
+	}
+}
+
+// getObjectArray retrieves an array of objects.
+func (c object) getObjectArray(name string) []object {
+	s, ok := c.get(name).([]interface{})
+	if !ok {
+		return nil
+	}
+	n := make([]object, len(s))
+	for i, o := range s {
+		if x, ok := o.(object); ok {
+			n[i] = x
+			continue
+		}
+		return nil
+	}
+	return n
+}
+
+// append adds objects to an array.
+func (c object) append(name string, values ...interface{}) error {
+	obj := c.get(name)
+	if obj == nil {
+		obj = []interface{}{}
+	}
+	switch array := obj.(type) {
+	case []interface{}:
+		return c.set(name, append(array, values...))
+	default:
+		return fmt.Errorf("%q is not an array", name)
+	}
+}
diff --git a/services/cluster/vkube/object_test.go b/services/cluster/vkube/object_test.go
new file mode 100644
index 0000000..62be6ee
--- /dev/null
+++ b/services/cluster/vkube/object_test.go
@@ -0,0 +1,112 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"testing"
+)
+
+func TestObject(t *testing.T) {
+	o := make(object)
+	o.set("foo", "bar")
+	o.set("slice", []interface{}{"a", "b", "c"})
+	o.set("obj", object{"name": "Bob"})
+	o.set("x.y.z", 5)
+	o.append("slice", "d")
+
+	out, err := o.json()
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	expected := `{
+  "foo": "bar",
+  "obj": {
+    "name": "Bob"
+  },
+  "slice": [
+    "a",
+    "b",
+    "c",
+    "d"
+  ],
+  "x": {
+    "y": {
+      "z": 5
+    }
+  }
+}`
+	if got := string(out); got != expected {
+		t.Errorf("Unexpected output. Got %q, expected %q", got, expected)
+	}
+}
+
+func TestObjectJSON(t *testing.T) {
+	json := `{
+          "foo": "bar",
+          "bar": 10,
+          "list": [ { "x":0 }, { "x":1 }, { "x":2 } ],
+          "x": { "y": [ 1, 2, 3 ] }
+        }`
+
+	o := make(object)
+	if err := o.importJSON([]byte(json)); err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if got, expected := o.getString("foo"), "bar"; got != expected {
+		t.Errorf("Unexpected value. Got %#v, expected %#v", got, expected)
+	}
+	if got, expected := o.getInt("bar"), 10; got != expected {
+		t.Errorf("Unexpected value. Got %#v, expected %#v", got, expected)
+	}
+	if got, expected := o.getString("notthere"), ""; got != expected {
+		t.Errorf("Unexpected value. Got %#v, expected %#v", got, expected)
+	}
+	if got, expected := o.getString("x.y"), "[1 2 3]"; got != expected {
+		t.Errorf("Unexpected value. Got %#v, expected %#v", got, expected)
+	}
+	o.append("x.y", 4)
+	list := o.getObjectArray("list")
+	for i, item := range list {
+		if got, expected := item.get("x"), float64(i); got != expected {
+			t.Errorf("Unexpected value for x. Got %#v, expected %#v", got, expected)
+		}
+	}
+	list = append(list, object{"x": "y"})
+	o.set("list", list)
+
+	out, err := o.json()
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	expected := `{
+  "bar": 10,
+  "foo": "bar",
+  "list": [
+    {
+      "x": 0
+    },
+    {
+      "x": 1
+    },
+    {
+      "x": 2
+    },
+    {
+      "x": "y"
+    }
+  ],
+  "x": {
+    "y": [
+      1,
+      2,
+      3,
+      4
+    ]
+  }
+}`
+	if got := string(out); got != expected {
+		t.Errorf("Unexpected output. Got %q, expected %q", got, expected)
+	}
+}
diff --git a/services/cluster/vkube/util.go b/services/cluster/vkube/util.go
new file mode 100644
index 0000000..cd69360
--- /dev/null
+++ b/services/cluster/vkube/util.go
@@ -0,0 +1,334 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"bytes"
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"os/exec"
+	"strings"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/rpc"
+	"v.io/v23/security"
+	"v.io/v23/vom"
+	"v.io/x/ref/services/cluster"
+)
+
+// getCredentials uses the gcloud command to get the credentials required to
+// access the kubernetes cluster.
+func getCredentials(cluster, project, zone string) error {
+	if out, err := exec.Command(flagGcloudBin, "config", "set", "container/cluster", cluster).CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to set container/cluster: %v: %s", err, out)
+	}
+	if out, err := exec.Command(flagGcloudBin, "container", "clusters", "get-credentials", cluster, "--project", project, "--zone", zone).CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to set get credentials for %q: %v: %s", cluster, err, out)
+	}
+	return nil
+}
+
+// localAgentAddress returns the address of the cluster agent to use from within
+// the cluster.
+func localAgentAddress(config *vkubeConfig) string {
+	return fmt.Sprintf("/(%s)@%s.%s:%d",
+		config.ClusterAgent.Blessing,
+		clusterAgentServiceName,
+		config.ClusterAgent.Namespace,
+		clusterAgentServicePort,
+	)
+}
+
+// readReplicationControllerConfig reads a ReplicationController config from a
+// file.
+func readReplicationControllerConfig(fileName string) (object, error) {
+	data, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		return nil, err
+	}
+	var rc object
+	if err := rc.importJSON(data); err != nil {
+		return nil, err
+	}
+	if kind := rc.getString("kind"); kind != "ReplicationController" {
+		return nil, fmt.Errorf("expected kind=\"ReplicationController\", got %q", kind)
+	}
+	return rc, nil
+}
+
+// addPodAgent takes either a ReplicationController or Pod object and adds a
+// pod-agent container to it. The existing containers are updated to use the
+// pod agent.
+func addPodAgent(ctx *context.T, config *vkubeConfig, obj object, secretName string) error {
+	var base string
+	switch kind := obj.getString("kind"); kind {
+	case "ReplicationController":
+		base = "spec.template."
+	case "Pod":
+		base = ""
+	default:
+		return fmt.Errorf("expected kind=\"ReplicationController\" or \"Pod\", got %q", kind)
+	}
+
+	// Add the volumes used by the pod agent container.
+	if err := obj.append(base+"spec.volumes",
+		object{"name": "agent-logs", "emptyDir": object{}},
+		object{"name": "agent-secret", "secret": object{"secretName": secretName}},
+		object{"name": "agent-socket", "emptyDir": object{}},
+	); err != nil {
+		return err
+	}
+
+	// Update the existing containers to talk to the pod agent.
+	containers := obj.getObjectArray(base + "spec.containers")
+	for _, c := range containers {
+		if err := c.append("env", object{"name": "V23_AGENT_PATH", "value": "/agent/socket/agent.sock"}); err != nil {
+			return err
+		}
+		if err := c.append("volumeMounts", object{"name": "agent-socket", "mountPath": "/agent/socket", "readOnly": true}); err != nil {
+			return err
+		}
+	}
+
+	// Add the pod agent container.
+	containers = append(containers, object{
+		"name":  "pod-agent",
+		"image": config.PodAgent.Image,
+		"args": []string{
+			"pod_agentd",
+			"--agent=" + localAgentAddress(config),
+			"--root-blessings=" + rootBlessings(ctx),
+			"--secret-key-file=/agent/secret/secret",
+			"--socket-path=/agent/socket/agent.sock",
+			"--log_dir=/logs",
+		},
+		"volumeMounts": []object{
+			object{"name": "agent-logs", "mountPath": "/logs"},
+			object{"name": "agent-secret", "mountPath": "/agent/secret", "readOnly": true},
+			object{"name": "agent-socket", "mountPath": "/agent/socket"},
+		},
+	})
+	return obj.set(base+"spec.containers", containers)
+}
+
+// createSecret gets a new secret key from the cluster agent, and then creates a
+// Secret object on kubernetes with it.
+func createSecret(ctx *context.T, secretName, namespace, agentAddr, extension string) error {
+	secret, err := cluster.ClusterAgentAdminClient(agentAddr).NewSecret(ctx, &granter{extension: extension})
+	if err != nil {
+		return err
+	}
+	if out, err := kubectlCreate(object{
+		"apiVersion": "v1",
+		"kind":       "Secret",
+		"metadata": object{
+			"name":      secretName,
+			"namespace": namespace,
+		},
+		"type": "Opaque",
+		"data": object{
+			"secret": base64.StdEncoding.EncodeToString([]byte(secret)),
+		},
+	}); err != nil {
+		return fmt.Errorf("failed to create secret %q: %v\n%s\n", secretName, err, string(out))
+	}
+	return nil
+}
+
+type granter struct {
+	rpc.CallOpt
+	extension string
+}
+
+func (g *granter) Grant(ctx *context.T, call security.Call) (security.Blessings, error) {
+	p := call.LocalPrincipal()
+	return p.Bless(call.RemoteBlessings().PublicKey(), p.BlessingStore().Default(), g.extension, security.UnconstrainedUse())
+}
+
+// deleteSecret deletes a Secret object and its associated secret key and
+// blessings.
+// We know the name of the Secret object, but we don't know the secret key. The
+// only way to get it back from Kubernetes is to mount the Secret Object to a
+// Pod, and then use the secret key to delete the secret key.
+func deleteSecret(ctx *context.T, config *vkubeConfig, name, namespace string) error {
+	podName := fmt.Sprintf("delete-secret-%s", name)
+	del := object{
+		"apiVersion": "v1",
+		"kind":       "Pod",
+		"metadata": object{
+			"name":      podName,
+			"namespace": namespace,
+		},
+		"spec": object{
+			"containers": []interface{}{
+				object{
+					"name":  "delete-secret",
+					"image": config.ClusterAgent.Image,
+					"args": []string{
+						"/bin/bash",
+						"-c",
+						"cluster_agent --agent='" + localAgentAddress(config) + "' forget $(cat /agent/secret/secret) && /google-cloud-sdk/bin/kubectl --namespace=" + namespace + " delete secret " + name + " && /google-cloud-sdk/bin/kubectl --namespace=" + namespace + " delete pod " + podName,
+					},
+					"volumeMounts": []interface{}{
+						object{"name": "agent-secret", "mountPath": "/agent/secret", "readOnly": true},
+					},
+				},
+			},
+			"restartPolicy":         "OnFailure",
+			"activeDeadlineSeconds": 300,
+		},
+	}
+	if err := addPodAgent(ctx, config, del, name); err != nil {
+		return err
+	}
+	out, err := kubectlCreate(del)
+	if err != nil {
+		return fmt.Errorf("failed to create delete Pod: %v: %s", err, out)
+	}
+	return nil
+}
+
+// createReplicationController takes a ReplicationController object, adds a
+// pod-agent, and then creates it on kubernetes.
+func createReplicationController(ctx *context.T, config *vkubeConfig, rc object, secretName string) error {
+	if err := addPodAgent(ctx, config, rc, secretName); err != nil {
+		return err
+	}
+	if out, err := kubectlCreate(rc); err != nil {
+		return fmt.Errorf("failed to create replication controller: %v\n%s\n", err, string(out))
+	}
+	return nil
+}
+
+// updateReplicationController takes a ReplicationController object, adds a
+// pod-agent, and then performs a rolling update.
+func updateReplicationController(ctx *context.T, config *vkubeConfig, rc object) error {
+	oldName, err := findReplicationControllerNameForApp(rc.getString("spec.template.metadata.labels.application"), rc.getString("metadata.namespace"))
+	if err != nil {
+		return err
+	}
+	secretName, err := findSecretName(oldName, rc.getString("metadata.namespace"))
+	if err != nil {
+		return err
+	}
+	if err := addPodAgent(ctx, config, rc, secretName); err != nil {
+		return err
+	}
+	json, err := rc.json()
+	if err != nil {
+		return err
+	}
+	cmd := exec.Command(flagKubectlBin, "rolling-update", oldName, "-f", "-")
+	cmd.Stdin = bytes.NewBuffer(json)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to update replication controller %q: %v\n%s\n", oldName, err, string(out))
+	}
+	return nil
+}
+
+// createNamespaceIfNotExist creates a Namespace object if it doesn't already exist.
+func createNamespaceIfNotExist(name string) error {
+	if _, err := kubectl("get", "namespace", name); err == nil {
+		return nil
+	}
+	if out, err := kubectlCreate(object{
+		"apiVersion": "v1",
+		"kind":       "Namespace",
+		"metadata": object{
+			"name": name,
+		},
+	}); err != nil {
+		return fmt.Errorf("failed to create Namespace %q: %v: %s", name, err, out)
+	}
+	return nil
+}
+
+// makeSecretName creates a random name for a Secret Object.
+func makeSecretName() (string, error) {
+	b := make([]byte, 16)
+	if _, err := rand.Read(b); err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("secret-%s", hex.EncodeToString(b)), nil
+}
+
+// findReplicationControllerNameForApp returns the name of the
+// ReplicationController that is currently used to run the given application.
+func findReplicationControllerNameForApp(app, namespace string) (string, error) {
+	data, err := kubectl("--namespace="+namespace, "get", "rc", "-l", "application="+app, "-o", "json")
+	if err != nil {
+		return "", fmt.Errorf("failed to get replication controller for application %q: %v\n%s\n", app, err, string(data))
+	}
+	var list object
+	if err := list.importJSON(data); err != nil {
+		return "", fmt.Errorf("failed to parse kubectl output: %v", err)
+	}
+	items := list.getObjectArray("items")
+	if c := len(items); c != 1 {
+		return "", fmt.Errorf("found %d replication controllers for application %q", c, app)
+	}
+	name := items[0].getString("metadata.name")
+	if name == "" {
+		return "", fmt.Errorf("missing metadata.name")
+	}
+	return name, nil
+}
+
+// findSecretName finds the name of the Secret Object associated the given
+// Replication Controller.
+func findSecretName(rcName, namespace string) (string, error) {
+	data, err := kubectl("--namespace="+namespace, "get", "rc", rcName, "-o", "json")
+	if err != nil {
+		return "", fmt.Errorf("failed to get replication controller %q: %v\n%s\n", rcName, err, string(data))
+	}
+	var rc object
+	if err := rc.importJSON(data); err != nil {
+		return "", fmt.Errorf("failed to parse kubectl output: %v", err)
+	}
+	for _, v := range rc.getObjectArray("spec.template.spec.volumes") {
+		if v.getString("name") == "agent-secret" {
+			return v.getString("secret.secretName"), nil
+		}
+	}
+	return "", fmt.Errorf("failed to find secretName in replication controller %q", rcName)
+}
+
+// kubectlCreate runs 'kubectl create -f' on the given object and returns the
+// output.
+func kubectlCreate(o object) ([]byte, error) {
+	json, err := o.json()
+	if err != nil {
+		return nil, err
+	}
+	cmd := exec.Command(flagKubectlBin, "create", "-f", "-")
+	cmd.Stdin = bytes.NewBuffer(json)
+	return cmd.CombinedOutput()
+}
+
+// kubectl runs the 'kubectl' command with the given arguments and returns the
+// output.
+func kubectl(args ...string) ([]byte, error) {
+	return exec.Command(flagKubectlBin, args...).CombinedOutput()
+}
+
+// rootBlessings returns the root blessings for the current principal.
+func rootBlessings(ctx *context.T) string {
+	p := v23.GetPrincipal(ctx)
+	b64 := []string{}
+	for _, root := range security.RootBlessings(p.BlessingStore().Default()) {
+		data, err := vom.Encode(root)
+		if err != nil {
+			ctx.Fatalf("vom.Encode failed: %v", err)
+		}
+		// We use URLEncoding to be compatible with the principal
+		// command.
+		b64 = append(b64, base64.URLEncoding.EncodeToString(data))
+	}
+	return strings.Join(b64, ",")
+}
diff --git a/services/cluster/vkube/util_test.go b/services/cluster/vkube/util_test.go
new file mode 100644
index 0000000..f43cb54
--- /dev/null
+++ b/services/cluster/vkube/util_test.go
@@ -0,0 +1,203 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"v.io/x/ref/test"
+)
+
+func TestAddPodAgent(t *testing.T) {
+	ctx, shutdown := test.V23Init()
+	defer shutdown()
+
+	const (
+		myAppJSON = `{
+  "apiVersion": "v1",
+  "kind": "ReplicationController",
+  "metadata": {
+    "name": "my-app",
+    "labels": {
+      "run": "my-app"
+    }
+  },
+  "spec": {
+    "replicas": 5,
+    "template": {
+      "metadata": {
+        "labels": {
+          "run": "my-app"
+        }
+      },
+      "spec": {
+        "containers": [
+          {
+            "name": "my-app",
+            "image": "registry/me/my-app:latest",
+            "ports": [
+              { "containerPort": 8193, "hostPort": 8193 }
+            ],
+            "volumeMounts": [
+              { "name": "app-logs", "mountPath": "/logs" }
+            ]
+          }
+        ],
+        "volumes": [
+          { "name": "app-logs", "emptyDir": {} }
+        ]
+      }
+    }
+  }
+}`
+
+		expected = `{
+  "apiVersion": "v1",
+  "kind": "ReplicationController",
+  "metadata": {
+    "labels": {
+      "run": "my-app"
+    },
+    "name": "my-app"
+  },
+  "spec": {
+    "replicas": 5,
+    "template": {
+      "metadata": {
+        "labels": {
+          "run": "my-app"
+        }
+      },
+      "spec": {
+        "containers": [
+          {
+            "env": [
+              {
+                "name": "V23_AGENT_PATH",
+                "value": "/agent/socket/agent.sock"
+              }
+            ],
+            "image": "registry/me/my-app:latest",
+            "name": "my-app",
+            "ports": [
+              {
+                "containerPort": 8193,
+                "hostPort": 8193
+              }
+            ],
+            "volumeMounts": [
+              {
+                "mountPath": "/logs",
+                "name": "app-logs"
+              },
+              {
+                "mountPath": "/agent/socket",
+                "name": "agent-socket",
+                "readOnly": true
+              }
+            ]
+          },
+          {
+            "args": [
+              "pod_agentd",
+              "--agent=/(root/cluster-agent)@cluster-agent.test:8193",
+              "--root-blessings=ROOT-BLESSINGS",
+              "--secret-key-file=/agent/secret/secret",
+              "--socket-path=/agent/socket/agent.sock",
+              "--log_dir=/logs"
+            ],
+            "image": "",
+            "name": "pod-agent",
+            "volumeMounts": [
+              {
+                "mountPath": "/logs",
+                "name": "agent-logs"
+              },
+              {
+                "mountPath": "/agent/secret",
+                "name": "agent-secret",
+                "readOnly": true
+              },
+              {
+                "mountPath": "/agent/socket",
+                "name": "agent-socket"
+              }
+            ]
+          }
+        ],
+        "volumes": [
+          {
+            "emptyDir": {},
+            "name": "app-logs"
+          },
+          {
+            "emptyDir": {},
+            "name": "agent-logs"
+          },
+          {
+            "name": "agent-secret",
+            "secret": {
+              "secretName": "myapp-secret"
+            }
+          },
+          {
+            "emptyDir": {},
+            "name": "agent-socket"
+          }
+        ]
+      }
+    }
+  }
+}`
+	)
+
+	var myAppObj object
+	if err := myAppObj.importJSON([]byte(myAppJSON)); err != nil {
+		t.Fatalf("importJSON failed: %v", err)
+	}
+
+	config := &vkubeConfig{
+		ClusterAgent: clusterAgentConfig{
+			Blessing:  "root/cluster-agent",
+			Namespace: "test",
+		},
+	}
+	if err := addPodAgent(ctx, config, myAppObj, "myapp-secret"); err != nil {
+		t.Fatalf("addPodAgent failed: %v", err)
+	}
+	outBytes, err := myAppObj.json()
+	if err != nil {
+		t.Fatalf("json failed: %v", err)
+	}
+	got := strings.Replace(string(outBytes), rootBlessings(ctx), "ROOT-BLESSINGS", 1)
+
+	if got != expected {
+		t.Errorf("unexpected output. Got %s, expected %s", got, expected)
+		diff(t, expected, got)
+	}
+}
+
+func diff(t *testing.T, expected, got string) {
+	dir, err := ioutil.TempDir("", "diff-")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+	expectedFile := filepath.Join(dir, "expected")
+	if err := ioutil.WriteFile(expectedFile, []byte(expected), 0644); err != nil {
+		t.Fatal(err)
+	}
+	gotFile := filepath.Join(dir, "got")
+	if err := ioutil.WriteFile(gotFile, []byte(got), 0644); err != nil {
+		t.Fatal(err)
+	}
+	out, _ := exec.Command("diff", "-u", expectedFile, gotFile).CombinedOutput()
+	t.Log(string(out))
+}