diff options
Diffstat (limited to 'client/go/cmd/prod.go')
-rw-r--r-- | client/go/cmd/prod.go | 304 |
1 files changed, 149 insertions, 155 deletions
diff --git a/client/go/cmd/prod.go b/client/go/cmd/prod.go index 4ce126bebb4..9108cb7a222 100644 --- a/client/go/cmd/prod.go +++ b/client/go/cmd/prod.go @@ -6,6 +6,7 @@ import ( "bytes" "errors" "fmt" + "io" "io/ioutil" "log" "os" @@ -19,32 +20,29 @@ import ( "github.com/vespa-engine/vespa/client/go/vespa/xml" ) -func init() { - rootCmd.AddCommand(prodCmd) - prodCmd.AddCommand(prodInitCmd) - prodCmd.AddCommand(prodSubmitCmd) -} - -var prodCmd = &cobra.Command{ - Use: "prod", - Short: "Deploy an application package to production in Vespa Cloud", - Long: `Deploy an application package to production in Vespa Cloud. +func newProdCmd() *cobra.Command { + return &cobra.Command{ + Use: "prod", + Short: "Deploy an application package to production in Vespa Cloud", + Long: `Deploy an application package to production in Vespa Cloud. Configure and deploy your application package to production in Vespa Cloud.`, - Example: `$ vespa prod init + Example: `$ vespa prod init $ vespa prod submit`, - DisableAutoGenTag: true, - SilenceUsage: false, - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("invalid command: %s", args[0]) - }, + DisableAutoGenTag: true, + SilenceUsage: false, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("invalid command: %s", args[0]) + }, + } } -var prodInitCmd = &cobra.Command{ - Use: "init", - Short: "Modify service.xml and deployment.xml for production deployment", - Long: `Modify service.xml and deployment.xml for production deployment. +func newProdInitCmd(cli *CLI) *cobra.Command { + return &cobra.Command{ + Use: "init", + Short: "Modify service.xml and deployment.xml for production deployment", + Long: `Modify service.xml and deployment.xml for production deployment. Only basic deployment configuration is available through this command. For advanced configuration see the relevant Vespa Cloud documentation and make @@ -53,62 +51,64 @@ changes to deployment.xml and services.xml directly. Reference: https://cloud.vespa.ai/en/reference/services https://cloud.vespa.ai/en/reference/deployment`, - DisableAutoGenTag: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - appSource := applicationSource(args) - pkg, err := vespa.FindApplicationPackage(appSource, false) - if err != nil { - return err - } - if pkg.IsZip() { - return errHint(fmt.Errorf("cannot modify compressed application package %s", pkg.Path), - "Try running 'mvn clean' and run this command again") - } + DisableAutoGenTag: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + appSource := applicationSource(args) + pkg, err := vespa.FindApplicationPackage(appSource, false) + if err != nil { + return err + } + if pkg.IsZip() { + return errHint(fmt.Errorf("cannot modify compressed application package %s", pkg.Path), + "Try running 'mvn clean' and run this command again") + } - deploymentXML, err := readDeploymentXML(pkg) - if err != nil { - return fmt.Errorf("could not read deployment.xml: %w", err) - } - servicesXML, err := readServicesXML(pkg) - if err != nil { - return fmt.Errorf("a services.xml declaring your cluster(s) must exist: %w", err) - } - target, err := getTarget() - if err != nil { - return err - } + deploymentXML, err := readDeploymentXML(pkg) + if err != nil { + return fmt.Errorf("could not read deployment.xml: %w", err) + } + servicesXML, err := readServicesXML(pkg) + if err != nil { + return fmt.Errorf("a services.xml declaring your cluster(s) must exist: %w", err) + } + target, err := cli.target(targetOptions{noCertificate: true}) + if err != nil { + return err + } - fmt.Fprint(stdout, "This will modify any existing ", color.YellowString("deployment.xml"), " and ", color.YellowString("services.xml"), - "!\nBefore modification a backup of the original file will be created.\n\n") - fmt.Fprint(stdout, "A default value is suggested (shown inside brackets) based on\nthe files' existing contents. Press enter to use it.\n\n") - fmt.Fprint(stdout, "Abort the configuration at any time by pressing Ctrl-C. The\nfiles will remain untouched.\n\n") - fmt.Fprint(stdout, "See this guide for sizing a Vespa deployment:\n", color.GreenString("https://docs.vespa.ai/en/performance/sizing-search.html\n\n")) - r := bufio.NewReader(stdin) - deploymentXML, err = updateRegions(r, deploymentXML, target.Deployment().System) - if err != nil { - return err - } - servicesXML, err = updateNodes(r, servicesXML) - if err != nil { - return err - } + fmt.Fprint(cli.Stdout, "This will modify any existing ", color.YellowString("deployment.xml"), " and ", color.YellowString("services.xml"), + "!\nBefore modification a backup of the original file will be created.\n\n") + fmt.Fprint(cli.Stdout, "A default value is suggested (shown inside brackets) based on\nthe files' existing contents. Press enter to use it.\n\n") + fmt.Fprint(cli.Stdout, "Abort the configuration at any time by pressing Ctrl-C. The\nfiles will remain untouched.\n\n") + fmt.Fprint(cli.Stdout, "See this guide for sizing a Vespa deployment:\n", color.GreenString("https://docs.vespa.ai/en/performance/sizing-search.html\n\n")) + r := bufio.NewReader(cli.Stdin) + deploymentXML, err = updateRegions(cli, r, deploymentXML, target.Deployment().System) + if err != nil { + return err + } + servicesXML, err = updateNodes(cli, r, servicesXML) + if err != nil { + return err + } - fmt.Fprintln(stdout) - if err := writeWithBackup(pkg, "deployment.xml", deploymentXML.String()); err != nil { - return err - } - if err := writeWithBackup(pkg, "services.xml", servicesXML.String()); err != nil { - return err - } - return nil - }, + fmt.Fprintln(cli.Stdout) + if err := writeWithBackup(cli.Stdout, pkg, "deployment.xml", deploymentXML.String()); err != nil { + return err + } + if err := writeWithBackup(cli.Stdout, pkg, "services.xml", servicesXML.String()); err != nil { + return err + } + return nil + }, + } } -var prodSubmitCmd = &cobra.Command{ - Use: "submit", - Short: "Submit your application for production deployment", - Long: `Submit your application for production deployment. +func newProdSubmitCmd(cli *CLI) *cobra.Command { + return &cobra.Command{ + Use: "submit", + Short: "Submit your application for production deployment", + Long: `Submit your application for production deployment. This commands uploads your application package to Vespa Cloud and deploys it to the production zones specified in deployment.xml. @@ -123,58 +123,52 @@ by a continuous build system. For more information about production deployments in Vespa Cloud see: https://cloud.vespa.ai/en/getting-to-production https://cloud.vespa.ai/en/automated-deployments`, - DisableAutoGenTag: true, - SilenceUsage: true, - Example: `$ mvn package # when adding custom Java components + DisableAutoGenTag: true, + SilenceUsage: true, + Example: `$ mvn package # when adding custom Java components $ vespa prod submit`, - RunE: func(cmd *cobra.Command, args []string) error { - target, err := getTarget() - if err != nil { - return err - } - if target.Type() != vespa.TargetCloud { - // TODO: Add support for hosted - return fmt.Errorf("prod submit does not support %s target", target.Type()) - } - appSource := applicationSource(args) - pkg, err := vespa.FindApplicationPackage(appSource, true) - if err != nil { - return err - } - cfg, err := LoadConfig() - if err != nil { - return err - } - if !pkg.HasDeployment() { - return errHint(fmt.Errorf("no deployment.xml found"), "Try creating one with vespa prod init") - } - if pkg.TestPath == "" { - return errHint(fmt.Errorf("no tests found"), - "The application must be a Java maven project, or include basic HTTP tests under src/test/application/", - "See https://cloud.vespa.ai/en/getting-to-production") - } - if err := verifyTests(pkg, target); err != nil { - return err - } - if !isCI() { - printWarning("We recommend doing this only from a CD job", "See https://cloud.vespa.ai/en/getting-to-production") - } - opts, err := getDeploymentOptions(cfg, pkg, target) - if err != nil { - return err - } - if err := vespa.Submit(opts); err != nil { - return fmt.Errorf("could not submit application for deployment: %w", err) - } else { - printSuccess("Submitted ", color.CyanString(pkg.Path), " for deployment") - log.Printf("See %s for deployment progress\n", color.CyanString(fmt.Sprintf("%s/tenant/%s/application/%s/prod/deployment", - opts.Target.Deployment().System.ConsoleURL, opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application))) - } - return nil - }, + RunE: func(cmd *cobra.Command, args []string) error { + target, err := cli.target(targetOptions{noCertificate: true}) + if err != nil { + return err + } + if target.Type() != vespa.TargetCloud { + // TODO: Add support for hosted + return fmt.Errorf("prod submit does not support %s target", target.Type()) + } + appSource := applicationSource(args) + pkg, err := vespa.FindApplicationPackage(appSource, true) + if err != nil { + return err + } + if !pkg.HasDeployment() { + return errHint(fmt.Errorf("no deployment.xml found"), "Try creating one with vespa prod init") + } + if pkg.TestPath == "" { + return errHint(fmt.Errorf("no tests found"), + "The application must be a Java maven project, or include basic HTTP tests under src/test/application/", + "See https://cloud.vespa.ai/en/getting-to-production") + } + if err := verifyTests(cli, pkg); err != nil { + return err + } + if !cli.isCI() { + cli.printWarning("We recommend doing this only from a CD job", "See https://cloud.vespa.ai/en/getting-to-production") + } + opts := cli.createDeploymentOptions(pkg, target) + if err := vespa.Submit(opts); err != nil { + return fmt.Errorf("could not submit application for deployment: %w", err) + } else { + cli.printSuccess("Submitted ", color.CyanString(pkg.Path), " for deployment") + log.Printf("See %s for deployment progress\n", color.CyanString(fmt.Sprintf("%s/tenant/%s/application/%s/prod/deployment", + opts.Target.Deployment().System.ConsoleURL, opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application))) + } + return nil + }, + } } -func writeWithBackup(pkg vespa.ApplicationPackage, filename, contents string) error { +func writeWithBackup(stdout io.Writer, pkg vespa.ApplicationPackage, filename, contents string) error { dst := filepath.Join(pkg.Path, filename) if util.PathExists(dst) { data, err := ioutil.ReadFile(dst) @@ -205,8 +199,8 @@ func writeWithBackup(pkg vespa.ApplicationPackage, filename, contents string) er return ioutil.WriteFile(dst, []byte(contents), 0644) } -func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (xml.Deployment, error) { - regions, err := promptRegions(r, deploymentXML, system) +func updateRegions(cli *CLI, stdin *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (xml.Deployment, error) { + regions, err := promptRegions(cli, stdin, deploymentXML, system) if err != nil { return xml.Deployment{}, err } @@ -225,10 +219,10 @@ func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.S return deploymentXML, nil } -func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (string, error) { - fmt.Fprintln(stdout, color.CyanString("> Deployment regions")) - fmt.Fprintf(stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/zones")) - fmt.Fprintf(stdout, "Example: %s\n\n", color.YellowString("aws-us-east-1c,aws-us-west-2a")) +func promptRegions(cli *CLI, stdin *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (string, error) { + fmt.Fprintln(cli.Stdout, color.CyanString("> Deployment regions")) + fmt.Fprintf(cli.Stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/zones")) + fmt.Fprintf(cli.Stdout, "Example: %s\n\n", color.YellowString("aws-us-east-1c,aws-us-west-2a")) var currentRegions []string for _, r := range deploymentXML.Prod.Regions { currentRegions = append(currentRegions, r.Name) @@ -247,12 +241,12 @@ func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.S } return nil } - return prompt(r, "Which regions do you wish to deploy in?", strings.Join(currentRegions, ","), validator) + return prompt(cli, stdin, "Which regions do you wish to deploy in?", strings.Join(currentRegions, ","), validator) } -func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error) { +func updateNodes(cli *CLI, r *bufio.Reader, servicesXML xml.Services) (xml.Services, error) { for _, c := range servicesXML.Container { - nodes, err := promptNodes(r, c.ID, c.Nodes) + nodes, err := promptNodes(cli, r, c.ID, c.Nodes) if err != nil { return xml.Services{}, err } @@ -261,7 +255,7 @@ func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error } } for _, c := range servicesXML.Content { - nodes, err := promptNodes(r, c.ID, c.Nodes) + nodes, err := promptNodes(cli, r, c.ID, c.Nodes) if err != nil { return xml.Services{}, err } @@ -272,8 +266,8 @@ func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error return servicesXML, nil } -func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml.Nodes, error) { - count, err := promptNodeCount(r, clusterID, defaultValue.Count) +func promptNodes(cli *CLI, r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml.Nodes, error) { + count, err := promptNodeCount(cli, r, clusterID, defaultValue.Count) if err != nil { return xml.Nodes{}, err } @@ -283,7 +277,7 @@ func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml if resources != nil { defaultSpec = defaultValue.Resources.String() } - spec, err := promptResources(r, clusterID, defaultSpec) + spec, err := promptResources(cli, r, clusterID, defaultSpec) if err != nil { return xml.Nodes{}, err } @@ -299,21 +293,21 @@ func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml return xml.Nodes{Count: count, Resources: resources}, nil } -func promptNodeCount(r *bufio.Reader, clusterID string, nodeCount string) (string, error) { - fmt.Fprintln(stdout, color.CyanString("\n> Node count: "+clusterID+" cluster")) - fmt.Fprintf(stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services")) - fmt.Fprintf(stdout, "Example: %s\nExample: %s\n\n", color.YellowString("4"), color.YellowString("[2,8]")) +func promptNodeCount(cli *CLI, stdin *bufio.Reader, clusterID string, nodeCount string) (string, error) { + fmt.Fprintln(cli.Stdout, color.CyanString("\n> Node count: "+clusterID+" cluster")) + fmt.Fprintf(cli.Stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services")) + fmt.Fprintf(cli.Stdout, "Example: %s\nExample: %s\n\n", color.YellowString("4"), color.YellowString("[2,8]")) validator := func(input string) error { _, _, err := xml.ParseNodeCount(input) return err } - return prompt(r, fmt.Sprintf("How many nodes should the %s cluster have?", color.CyanString(clusterID)), nodeCount, validator) + return prompt(cli, stdin, fmt.Sprintf("How many nodes should the %s cluster have?", color.CyanString(clusterID)), nodeCount, validator) } -func promptResources(r *bufio.Reader, clusterID string, resources string) (string, error) { - fmt.Fprintln(stdout, color.CyanString("\n> Node resources: "+clusterID+" cluster")) - fmt.Fprintf(stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services")) - fmt.Fprintf(stdout, "Example: %s\nExample: %s\n\n", color.YellowString("auto"), color.YellowString("vcpu=4,memory=8Gb,disk=100Gb")) +func promptResources(cli *CLI, stdin *bufio.Reader, clusterID string, resources string) (string, error) { + fmt.Fprintln(cli.Stdout, color.CyanString("\n> Node resources: "+clusterID+" cluster")) + fmt.Fprintf(cli.Stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services")) + fmt.Fprintf(cli.Stdout, "Example: %s\nExample: %s\n\n", color.YellowString("auto"), color.YellowString("vcpu=4,memory=8Gb,disk=100Gb")) validator := func(input string) error { if input == "auto" { return nil @@ -321,7 +315,7 @@ func promptResources(r *bufio.Reader, clusterID string, resources string) (strin _, err := xml.ParseResources(input) return err } - return prompt(r, fmt.Sprintf("Which resources should each node in the %s cluster have?", color.CyanString(clusterID)), resources, validator) + return prompt(cli, stdin, fmt.Sprintf("Which resources should each node in the %s cluster have?", color.CyanString(clusterID)), resources, validator) } func readDeploymentXML(pkg vespa.ApplicationPackage) (xml.Deployment, error) { @@ -345,17 +339,17 @@ func readServicesXML(pkg vespa.ApplicationPackage) (xml.Services, error) { return xml.ReadServices(f) } -func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(input string) error) (string, error) { +func prompt(cli *CLI, stdin *bufio.Reader, question, defaultAnswer string, validator func(input string) error) (string, error) { var input string for input == "" { - fmt.Fprint(stdout, question) + fmt.Fprint(cli.Stdout, question) if defaultAnswer != "" { - fmt.Fprint(stdout, " [", color.YellowString(defaultAnswer), "]") + fmt.Fprint(cli.Stdout, " [", color.YellowString(defaultAnswer), "]") } - fmt.Fprint(stdout, " ") + fmt.Fprint(cli.Stdout, " ") var err error - input, err = r.ReadString('\n') + input, err = stdin.ReadString('\n') if err != nil { return "", err } @@ -365,15 +359,15 @@ func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(inpu } if err := validator(input); err != nil { - printErrHint(err) - fmt.Fprintln(stderr) + cli.printErrHint(err) + fmt.Fprintln(cli.Stderr) input = "" } } return input, nil } -func verifyTests(app vespa.ApplicationPackage, target vespa.Target) error { +func verifyTests(cli *CLI, app vespa.ApplicationPackage) error { // TODO: system-test, staging-setup and staging-test should be required if the application // does not have any Java tests. suites := map[string]bool{ @@ -392,14 +386,14 @@ func verifyTests(app vespa.ApplicationPackage, target vespa.Target) error { testPath = path } for suite, required := range suites { - if err := verifyTest(testPath, suite, target, required); err != nil { + if err := verifyTest(cli, testPath, suite, required); err != nil { return err } } return nil } -func verifyTest(testsParent string, suite string, target vespa.Target, required bool) error { +func verifyTest(cli *CLI, testsParent string, suite string, required bool) error { testDirectory := filepath.Join(testsParent, "tests", suite) _, err := os.Stat(testDirectory) if err != nil { @@ -413,6 +407,6 @@ func verifyTest(testsParent string, suite string, target vespa.Target, required } return nil } - _, _, err = runTests(testDirectory, true) + _, _, err = runTests(cli, "", testDirectory, true) return err } |