services/cluster: Add vkube command.
This is a prototype that implements the basic functionality of a cluster
manager. The vkube command can be used to:
- start / stop the cluster agent.
- start / update / stop vanadium applications.
- build docker images for cluster & pod agents.
When vanadium application are started, vkube creates a new secret key on
the cluster agent, and a Secret object on Kubernetes.
This Secret object is then added to the user-provided
replication-controller config, along with a pod agent container and all
the hooks necessary for the vanadium app to talk to the pod agent.
Tested manually against a Kubernetes cluster. Still need to add a lot
tests.
Change-Id: I8fa6c00e8011c996eefe074184a2d8bdf1ba087f
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))
+}