| // 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, ",") |
| } |