| // 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" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "strings" |
| "time" |
| |
| "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 |
| flagGpg string |
| flagGetCredentials bool |
| flagNoBlessings bool |
| flagBaseBlessings string |
| flagNoHeaders bool |
| flagResourceFile string |
| flagVerbose bool |
| flagTag string |
| flagWait bool |
| flagWaitTimeout time.Duration |
| flagClusterAgentImage string |
| flagPodAgentImage string |
| flagSecrets string |
| flagPrompt bool |
| ) |
| |
| const ( |
| Deployment = "Deployment" |
| Pod = "Pod" |
| ) |
| |
| func main() { |
| cmdline.HideGlobalFlagsExcept() |
| |
| cmd := &cmdline.Command{ |
| Name: "vkube", |
| Short: "Manages Vanadium applications on kubernetes", |
| Long: "Manages Vanadium applications on kubernetes", |
| Children: []*cmdline.Command{ |
| cmdStart, |
| cmdUpdate, |
| cmdStop, |
| cmdStartClusterAgent, |
| cmdStopClusterAgent, |
| cmdUpdateClusterAgent, |
| cmdUpdateConfig, |
| cmdClaimClusterAgent, |
| cmdBuildDockerImages, |
| cmdCreateSecrets, |
| cmdUpgradeCluster, |
| cmdKubectl, |
| }, |
| } |
| 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.") |
| cmd.Flags.BoolVar(&flagGetCredentials, "get-credentials", true, "When true, use gcloud to get the cluster credentials. Otherwise, assume kubectl already has the correct credentials, and 'vkube kubectl' is equivalent to 'kubectl'.") |
| cmd.Flags.BoolVar(&flagNoHeaders, "no-headers", false, "When true, suppress the 'Project: ... Zone: ... Cluster: ...' headers.") |
| |
| cmdStart.Flags.BoolVar(&flagNoBlessings, "noblessings", false, "Do not pass blessings to the application.") |
| cmdStart.Flags.StringVar(&flagBaseBlessings, "base-blessings", "", "Base blessings to extend, base64url-vom-encoded. If empty, the default blessings are used.") |
| cmdStart.Flags.StringVar(&flagResourceFile, "f", "", "Filename to use to create the kubernetes resource.") |
| cmdStart.Flags.BoolVar(&flagWait, "wait", false, "Wait for all the replicas to be ready.") |
| cmdStart.Flags.DurationVar(&flagWaitTimeout, "wait-timeout", 20*time.Minute, "How long to wait for the start to make progress.") |
| |
| cmdUpdate.Flags.StringVar(&flagResourceFile, "f", "", "Filename to use to update the kubernetes resource.") |
| cmdUpdate.Flags.StringVar(&flagBaseBlessings, "base-blessings", "", "Base blessings to extend, base64url-vom-encoded. If empty, the default blessings are used.") |
| cmdUpdate.Flags.BoolVar(&flagWait, "wait", false, "Wait for the update to finish.") |
| cmdUpdate.Flags.DurationVar(&flagWaitTimeout, "wait-timeout", 20*time.Minute, "How long to wait for the update to make progress.") |
| |
| cmdStop.Flags.StringVar(&flagResourceFile, "f", "", "Filename to use to stop the kubernetes resource.") |
| |
| cmdStartClusterAgent.Flags.BoolVar(&flagWait, "wait", false, "Wait for the cluster agent to be ready.") |
| cmdStartClusterAgent.Flags.DurationVar(&flagWaitTimeout, "wait-timeout", 20*time.Minute, "How long to wait for the cluster agent to be ready.") |
| |
| cmdUpdateClusterAgent.Flags.BoolVar(&flagWait, "wait", false, "Wait for the cluster agent to be ready.") |
| cmdUpdateClusterAgent.Flags.DurationVar(&flagWaitTimeout, "wait-timeout", 20*time.Minute, "How long to wait for the cluster agent to be ready.") |
| |
| cmdUpdateConfig.Flags.StringVar(&flagClusterAgentImage, "cluster-agent-image", "", "The new cluster agent image. If the name starts with ':', only the image tag is updated.") |
| cmdUpdateConfig.Flags.StringVar(&flagPodAgentImage, "pod-agent-image", "", "The new pod agent image. If the name starts with ':', only the image tag is updated.") |
| |
| cmdBuildDockerImages.Flags.BoolVar(&flagVerbose, "v", false, "When true, the output is more verbose.") |
| cmdBuildDockerImages.Flags.StringVar(&flagTag, "tag", "", "The tag to add to the docker images. If empty, the current timestamp is used.") |
| |
| cmdCreateSecrets.Flags.StringVar(&flagSecrets, "secrets", "", "The file containing the encrypted secrets.") |
| cmdCreateSecrets.Flags.StringVar(&flagGpg, "gpg", "gpg", "The gpg binary to use.") |
| |
| cmdUpgradeCluster.Flags.BoolVar(&flagPrompt, "prompt", true, "When true, prompt for confirmation before upgrading the cluster.") |
| |
| cmdline.Main(cmd) |
| } |
| |
| func kubeCmdRunner(kcmd func(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error) cmdline.Runner { |
| return v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error { |
| config, err := readConfig(flagConfigFile) |
| if err != nil { |
| return err |
| } |
| if flagGetCredentials { |
| if !flagNoHeaders { |
| fmt.Fprintf(env.Stderr, "Project: %s Zone: %s Cluster: %s\n\n", config.Project, config.Zone, config.Cluster) |
| } |
| f, err := ioutil.TempFile("", "kubeconfig-") |
| if err != nil { |
| return err |
| } |
| os.Setenv("KUBECONFIG", f.Name()) |
| defer os.Remove(f.Name()) |
| f.Close() |
| |
| if out, err := exec.Command(flagGcloudBin, "container", "clusters", "get-credentials", config.Cluster, "--project", config.Project, "--zone", config.Zone).CombinedOutput(); err != nil { |
| return fmt.Errorf("failed to get credentials for %q: %v: %s", config.Cluster, err, out) |
| } |
| } |
| return kcmd(ctx, env, args, config) |
| }) |
| } |
| |
| var cmdStart = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdStart), |
| Name: "start", |
| Short: "Starts an application.", |
| Long: "Starts an application.", |
| ArgsName: "<extension>", |
| ArgsLong: `<extension> The blessing name extension to give to the application. |
| |
| If --noblessings is set, this argument is not needed. |
| `, |
| } |
| |
| func runCmdStart(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if flagNoBlessings && len(args) != 0 { |
| return env.UsageErrorf("start: no arguments are expected when --noblessings is set") |
| } |
| if !flagNoBlessings && len(args) != 1 { |
| return env.UsageErrorf("start: expected one argument, got %d", len(args)) |
| } |
| if flagResourceFile == "" { |
| return env.UsageErrorf("-f must be specified.") |
| } |
| dep, err := readResourceConfig(flagResourceFile) |
| if err != nil { |
| return err |
| } |
| namespace := dep.getString("metadata.namespace") |
| appName := dep.getString("spec.template.metadata.labels.application") |
| |
| if flagNoBlessings { |
| if out, err := kubectlCreate(dep); err != nil { |
| fmt.Fprintln(env.Stderr, string(out)) |
| return err |
| } |
| } else { |
| sm, err := newSecretManager(ctx, config, namespace) |
| if err != nil { |
| return err |
| } |
| secretName, err := sm.create(args[0]) |
| if err != nil { |
| return err |
| } |
| fmt.Fprintf(env.Stdout, "Created secret %q.\n", secretName) |
| |
| if err := createDeployment(ctx, config, dep, secretName); err != nil { |
| if err := sm.delete(secretName); err != nil { |
| fmt.Fprintf(env.Stderr, "Failed to delete secret %q: %v\n", secretName, err) |
| } else { |
| fmt.Fprintf(env.Stdout, "Deleted secret %q.\n", secretName) |
| } |
| return err |
| } |
| } |
| if flagWait { |
| if err := watchDeploymentRollout(appName, namespace, flagWaitTimeout, env.Stdout); err != nil { |
| return err |
| } |
| } |
| fmt.Fprintln(env.Stdout, "Created deployment successfully.") |
| return nil |
| } |
| |
| var cmdUpdate = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdUpdate), |
| Name: "update", |
| Short: "Updates an application.", |
| Long: "Updates an application to a new version with a rolling update.", |
| ArgsName: "[<extension>]", |
| ArgsLong: "<extension> The new blessing name extension to give to the application. If omitted, the existing blessings are preserved.", |
| } |
| |
| func runCmdUpdate(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if flagResourceFile == "" { |
| return env.UsageErrorf("-f must be specified.") |
| } |
| dep, err := readResourceConfig(flagResourceFile) |
| if err != nil { |
| return err |
| } |
| name := dep.getString("metadata.name") |
| namespace := dep.getString("metadata.namespace") |
| |
| oldSecretName, rootB, err := findPodAttributes(name, namespace) |
| if err != nil { |
| return err |
| } |
| |
| var ( |
| secretName string |
| sm *secretManager |
| ) |
| switch len(args) { |
| case 0: // re-use existing secret |
| secretName = oldSecretName |
| case 1: // create new secret |
| rootB = rootBlessings(ctx) |
| sm, err = newSecretManager(ctx, config, namespace) |
| if err != nil { |
| return err |
| } |
| if secretName, err = sm.create(args[0]); err != nil { |
| return err |
| } |
| fmt.Fprintf(env.Stdout, "Created new secret %q.\n", secretName) |
| default: |
| return env.UsageErrorf("update: expected at most one argument, got %d", len(args)) |
| } |
| |
| if err := updateDeployment(ctx, config, dep, secretName, rootB, env.Stdout, env.Stderr); err != nil { |
| if sm != nil { |
| if err := sm.delete(secretName); err != nil { |
| fmt.Fprintf(env.Stderr, "Failed to delete new secret %q: %v\n", secretName, err) |
| } else { |
| fmt.Fprintf(env.Stdout, "Deleted new secret %q.\n", secretName) |
| } |
| } |
| return err |
| } |
| if flagWait { |
| if err := watchDeploymentRollout(name, namespace, flagWaitTimeout, env.Stdout); err != nil { |
| return err |
| } |
| } |
| fmt.Fprintln(env.Stdout, "Updated deployment successfully.") |
| |
| if sm != nil && oldSecretName != "" { |
| if err := sm.delete(oldSecretName); err != nil { |
| fmt.Fprintf(env.Stderr, "Failed to delete old secret %q: %v\n", oldSecretName, err) |
| } else { |
| fmt.Fprintf(env.Stdout, "Deleted old secret %q.\n", oldSecretName) |
| } |
| } |
| |
| return nil |
| } |
| |
| var cmdStop = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdStop), |
| Name: "stop", |
| Short: "Stops an application.", |
| Long: "Stops an application.", |
| } |
| |
| func runCmdStop(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if flagResourceFile == "" { |
| return env.UsageErrorf("-f must be specified.") |
| } |
| dep, err := readResourceConfig(flagResourceFile) |
| if err != nil { |
| return err |
| } |
| name := dep.getString("metadata.name") |
| if name == "" { |
| return fmt.Errorf("metadata.name must be set") |
| } |
| namespace := dep.getString("metadata.namespace") |
| secretName, _, err := findPodAttributes(name, namespace) |
| if err != nil { |
| return err |
| } |
| if out, err := kubectl("--namespace="+namespace, "delete", "deployment", name); err != nil { |
| return fmt.Errorf("failed to stop deployment: %v: %s", err, out) |
| } |
| fmt.Fprintln(env.Stdout, "Stopped deployment.") |
| if secretName != "" { |
| sm, err := newSecretManager(ctx, config, namespace) |
| if err != nil { |
| return err |
| } |
| if err := sm.delete(secretName); err != nil { |
| fmt.Fprintf(env.Stderr, "Failed to delete secret %q: %v\n", secretName, err) |
| return err |
| } |
| fmt.Fprintf(env.Stdout, "Deleted secret %q.\n", secretName) |
| } |
| return nil |
| } |
| |
| var cmdStartClusterAgent = &cmdline.Command{ |
| Runner: kubeCmdRunner(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, config *vkubeConfig) error { |
| if err := createClusterAgent(ctx, config); err != nil { |
| return err |
| } |
| if !flagWait { |
| fmt.Fprintln(env.Stdout, "Starting cluster agent.") |
| return nil |
| } |
| if err := watchDeploymentRollout(clusterAgentApplicationName, config.ClusterAgent.Namespace, flagWaitTimeout, env.Stdout); err != nil { |
| return err |
| } |
| for { |
| if _, err := findClusterAgent(config, true); err == nil { |
| break |
| } |
| time.Sleep(time.Second) |
| } |
| fmt.Fprintf(env.Stdout, "Cluster agent is ready.\n") |
| return nil |
| } |
| |
| var cmdStopClusterAgent = &cmdline.Command{ |
| Runner: kubeCmdRunner(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, config *vkubeConfig) error { |
| if err := stopClusterAgent(config); err != nil { |
| return err |
| } |
| fmt.Fprintln(env.Stdout, "Stopping cluster agent.") |
| return nil |
| } |
| |
| var cmdUpdateClusterAgent = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdUpdateClusterAgent), |
| Name: "update-cluster-agent", |
| Short: "Updates the cluster agent.", |
| Long: "Updates the cluster agent.", |
| } |
| |
| func runCmdUpdateClusterAgent(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if err := updateClusterAgent(config, env.Stdout, env.Stderr); err != nil { |
| return err |
| } |
| if !flagWait { |
| fmt.Fprintln(env.Stdout, "Updating cluster agent.") |
| } |
| if err := watchDeploymentRollout(clusterAgentApplicationName, config.ClusterAgent.Namespace, flagWaitTimeout, env.Stdout); err != nil { |
| return err |
| } |
| for { |
| if _, err := findClusterAgent(config, true); err == nil { |
| break |
| } |
| time.Sleep(time.Second) |
| } |
| fmt.Fprintf(env.Stdout, "Updated cluster agent is ready.\n") |
| return nil |
| } |
| |
| var cmdClaimClusterAgent = &cmdline.Command{ |
| Runner: kubeCmdRunner(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, config *vkubeConfig) error { |
| 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.Fprintln(env.Stdout, "Claimed cluster agent successfully.") |
| return nil |
| } |
| |
| var cmdUpdateConfig = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdUpdateConfig), |
| Name: "update-config", |
| Short: "Updates vkube.cfg.", |
| Long: "Updates the vkube.cfg file with new cluster-agent and/or pod-agent images.", |
| } |
| |
| func runCmdUpdateConfig(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if flagClusterAgentImage != "" { |
| if flagClusterAgentImage[0] == ':' { |
| config.ClusterAgent.Image = removeTag(config.ClusterAgent.Image) + flagClusterAgentImage |
| } else { |
| config.ClusterAgent.Image = flagClusterAgentImage |
| } |
| } |
| if flagPodAgentImage != "" { |
| if flagPodAgentImage[0] == ':' { |
| config.PodAgent.Image = removeTag(config.PodAgent.Image) + flagPodAgentImage |
| } else { |
| config.PodAgent.Image = flagPodAgentImage |
| } |
| } |
| if err := writeConfig(flagConfigFile, config); err != nil { |
| return err |
| } |
| fmt.Fprintf(env.Stdout, "Updated %q.\n", flagConfigFile) |
| return nil |
| } |
| |
| var cmdBuildDockerImages = &cmdline.Command{ |
| Runner: kubeCmdRunner(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, config *vkubeConfig) error { |
| return buildDockerImages(config, flagTag, flagVerbose, env.Stdout) |
| } |
| |
| var cmdCreateSecrets = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCreateSecrets), |
| Name: "create-secrets", |
| Short: "Creates secrets", |
| Long: ` |
| Creates kubernetes secrets from the template files passed as arguments. |
| |
| The --secrets flag points to an encrypted TAR file that contains the actual |
| secrets. |
| |
| Each template file represents a kubernetes Secret object to be created by |
| substituting the template fields with the content of the files from the |
| encrypted TAR file. |
| |
| Templates look like: |
| |
| { |
| "apiVersion": "v1", |
| "kind": "Secret", |
| "metadata": { |
| "name": "secret-name" |
| }, |
| "type": "Opaque", |
| "data": { |
| "file1": "{{base64 "path/file1"}}", |
| "file2": "{{base64 "path/file2"}}" |
| } |
| } |
| |
| where path/file1 and path/file2 are files from the TAR file. |
| `, |
| ArgsName: "<template> ...", |
| ArgsLong: "<template> A file containing the template for the secret object.", |
| } |
| |
| func runCreateSecrets(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if len(args) == 0 { |
| return env.UsageErrorf("must specify at least one template file") |
| } |
| if flagSecrets == "" { |
| return env.UsageErrorf("must specify --secrets") |
| } |
| return createSecrets(flagSecrets, args) |
| } |
| |
| var cmdUpgradeCluster = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdUpgradeCluster), |
| Name: "upgrade-cluster", |
| Short: "Upgrade the cluster defined in vkube.cfg.", |
| Long: ` |
| Upgrade the cluster defined in vkube.cfg to the default latest version. |
| |
| This command is only meaningful with Kubernetes clusters on the Google Container |
| Engine. |
| `, |
| } |
| |
| func runCmdUpgradeCluster(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| if config.Project == "" { |
| return env.UsageErrorf("upgrade-cluster requires 'project' to be set in vkube.cfg") |
| } |
| return upgradeCluster(ctx, config, env.Stdin, env.Stdout, env.Stderr) |
| } |
| |
| var cmdKubectl = &cmdline.Command{ |
| Runner: kubeCmdRunner(runCmdKubectl), |
| Name: "kubectl", |
| Short: "Runs kubectl on the cluster defined in vkube.cfg.", |
| Long: "Runs kubectl on the cluster defined in vkube.cfg.", |
| ArgsName: "-- <kubectl args>", |
| ArgsLong: "<kubectl args> are passed directly to the kubectl command.", |
| } |
| |
| func runCmdKubectl(ctx *context.T, env *cmdline.Env, args []string, config *vkubeConfig) error { |
| cmd := exec.Command(flagKubectlBin, args...) |
| cmd.Stdin = env.Stdin |
| cmd.Stdout = env.Stdout |
| cmd.Stderr = env.Stderr |
| return cmd.Run() |
| } |