summaryrefslogtreecommitdiffstats
path: root/client/go/internal/admin
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2023-02-03 15:39:41 +0100
committerMartin Polden <mpolden@mpolden.no>2023-02-03 15:39:41 +0100
commit1f7228451d9e53022b1dd22c653d7b17628fa32b (patch)
treec06d30007c127c67d7928e2026603b19a0e3f514 /client/go/internal/admin
parentc64501aab06da35778cb2f6daa186218c133aaca (diff)
Move deploy to admin
Diffstat (limited to 'client/go/internal/admin')
-rw-r--r--client/go/internal/admin/deploy/activate.go44
-rw-r--r--client/go/internal/admin/deploy/cmd.go155
-rw-r--r--client/go/internal/admin/deploy/curl.go122
-rw-r--r--client/go/internal/admin/deploy/fetch.go96
-rw-r--r--client/go/internal/admin/deploy/options.go70
-rw-r--r--client/go/internal/admin/deploy/persist.go85
-rw-r--r--client/go/internal/admin/deploy/prepare.go83
-rw-r--r--client/go/internal/admin/deploy/results.go86
-rw-r--r--client/go/internal/admin/deploy/upload.go126
-rw-r--r--client/go/internal/admin/deploy/urls.go73
-rw-r--r--client/go/internal/admin/script-utils/main.go2
11 files changed, 941 insertions, 1 deletions
diff --git a/client/go/internal/admin/deploy/activate.go b/client/go/internal/admin/deploy/activate.go
new file mode 100644
index 00000000000..1f475ff0461
--- /dev/null
+++ b/client/go/internal/admin/deploy/activate.go
@@ -0,0 +1,44 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// main entry point for vespa-deploy activate
+
+func RunActivate(opts *Options, args []string) error {
+ var sessId string
+ if len(args) == 0 {
+ sessId = getSessionIdFromFile(opts.Tenant)
+ } else {
+ sessId = args[0]
+ }
+ src := makeConfigsourceUrl(opts)
+ url := src + pathPrefix(opts) + "/" + sessId + "/active"
+ url = addUrlPropertyFromFlag(url, opts.Verbose, "verbose")
+ url = addUrlPropertyFromOption(url, strconv.Itoa(opts.Timeout), "timeout")
+ fmt.Printf("Activating session %s using %s\n", sessId, urlWithoutQuery(url))
+ output, err := curlPutNothing(url)
+ if err != nil {
+ return err
+ }
+ var result ActivateResult
+ code, err := decodeResponse(output, &result)
+ if err != nil {
+ return err
+ }
+ if code == 200 {
+ fmt.Println(result.Message)
+ fmt.Println("Checksum: ", result.Application.Checksum)
+ fmt.Println("Timestamp: ", result.Deploy.Timestamp)
+ fmt.Println("Generation:", result.Application.Generation)
+ } else {
+ err = fmt.Errorf("Request failed. HTTP status code: %d\n%s", code, result.Message)
+ }
+ return err
+}
diff --git a/client/go/internal/admin/deploy/cmd.go b/client/go/internal/admin/deploy/cmd.go
new file mode 100644
index 00000000000..c4489d11771
--- /dev/null
+++ b/client/go/internal/admin/deploy/cmd.go
@@ -0,0 +1,155 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/internal/admin/trace"
+ "github.com/vespa-engine/vespa/client/go/internal/cli/build"
+ "github.com/vespa-engine/vespa/client/go/internal/util"
+ "github.com/vespa-engine/vespa/client/go/internal/vespa"
+)
+
+func reallySimpleHelp(cmd *cobra.Command, args []string) {
+ fmt.Println("Usage: vespa-deploy", cmd.Use)
+}
+
+func NewDeployCmd() *cobra.Command {
+ var (
+ curOptions Options
+ )
+ if err := vespa.LoadDefaultEnv(); err != nil {
+ util.JustExitWith(err)
+ }
+ cobra.EnableCommandSorting = false
+ cmd := &cobra.Command{
+ Use: "vespa-deploy [-h] [-v] [-f] [-t] [-c] [-p] [-z] [-V] [<command>] [args]",
+ Short: "deploy applications to vespa config server",
+ Long: `Usage: vespa-deploy [-h] [-v] [-f] [-t] [-c] [-p] [-z] [-V] [<command>] [args]
+Supported commands: 'upload', 'prepare', 'activate', 'fetch' and 'help'
+Supported options: '-h' (help), '-v' (verbose), '-f' (force/ignore validation errors), '-t' (timeout in seconds), '-p' (config server http port)
+Try 'vespa-deploy help <command>' to get more help`,
+ Version: build.Version,
+ Args: cobra.MaximumNArgs(2),
+ CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
+ }
+ cmd.PersistentFlags().BoolVarP(&curOptions.Verbose, "verbose", "v", false, "show details")
+ cmd.PersistentFlags().BoolVarP(&curOptions.DryRun, "dryrun", "n", false, "dry-run")
+ cmd.PersistentFlags().BoolVarP(&curOptions.Force, "force", "f", false, "ignore validation errors")
+ cmd.PersistentFlags().BoolVarP(&curOptions.Hosted, "hosted", "H", false, "for hosted vespa")
+
+ cmd.PersistentFlags().StringVarP(&curOptions.ServerHost, "server", "c", "", "config server hostname")
+ cmd.PersistentFlags().IntVarP(&curOptions.PortNumber, "port", "p", 19071, "config server http port")
+ cmd.PersistentFlags().IntVarP(&curOptions.Timeout, "timeout", "t", 900, "timeout in seconds")
+
+ cmd.PersistentFlags().StringVarP(&curOptions.Tenant, "tenant", "e", "default", "which tentant")
+ cmd.PersistentFlags().StringVarP(&curOptions.Region, "region", "r", "default", "which region")
+ cmd.PersistentFlags().StringVarP(&curOptions.Environment, "environment", "E", "prod", "which environment")
+ cmd.PersistentFlags().StringVarP(&curOptions.Application, "application", "a", "default", "which application")
+ cmd.PersistentFlags().StringVarP(&curOptions.Instance, "instance", "i", "default", "which instance")
+
+ cmd.PersistentFlags().StringVarP(&curOptions.Rotations, "rotations", "R", "", "which rotations")
+ cmd.PersistentFlags().StringVarP(&curOptions.VespaVersion, "vespaversion", "V", "", "which vespa version")
+
+ cmd.PersistentFlags().MarkHidden("hosted")
+ cmd.PersistentFlags().MarkHidden("rotations")
+ cmd.PersistentFlags().MarkHidden("vespaversion")
+
+ cmd.AddCommand(newUploadCmd(&curOptions))
+ cmd.AddCommand(newPrepareCmd(&curOptions))
+ cmd.AddCommand(newActivateCmd(&curOptions))
+ cmd.AddCommand(newFetchCmd(&curOptions))
+
+ cmd.InitDefaultHelpCmd()
+ return cmd
+}
+
+func newUploadCmd(opts *Options) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "upload <application package>",
+ Run: func(cmd *cobra.Command, args []string) {
+ opts.Command = CmdUpload
+ if opts.Verbose {
+ trace.AdjustVerbosity(1)
+ }
+ trace.Trace("upload with", opts, args)
+ err := RunUpload(opts, args)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+ os.Exit(1)
+ }
+ },
+ Args: cobra.MaximumNArgs(1),
+ }
+ cmd.Flags().StringVarP(&opts.From, "from", "F", "", `where from`)
+ cmd.SetHelpFunc(reallySimpleHelp)
+ return cmd
+}
+
+func newPrepareCmd(opts *Options) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "prepare [<session_id> | <application package>]",
+ Run: func(cmd *cobra.Command, args []string) {
+ opts.Command = CmdPrepare
+ if opts.Verbose {
+ trace.AdjustVerbosity(1)
+ }
+ trace.Trace("prepare with", opts, args)
+ err := RunPrepare(opts, args)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+ os.Exit(1)
+ }
+ },
+ Args: cobra.MaximumNArgs(1),
+ }
+ cmd.SetHelpFunc(reallySimpleHelp)
+ return cmd
+}
+
+func newActivateCmd(opts *Options) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "activate [<session_id>]",
+ Run: func(cmd *cobra.Command, args []string) {
+ opts.Command = CmdActivate
+ if opts.Verbose {
+ trace.AdjustVerbosity(1)
+ }
+ trace.Trace("activate with", opts, args)
+ err := RunActivate(opts, args)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+ os.Exit(1)
+ }
+ },
+ Args: cobra.MaximumNArgs(1),
+ }
+ cmd.SetHelpFunc(reallySimpleHelp)
+ return cmd
+}
+
+func newFetchCmd(opts *Options) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "fetch <output directory>",
+ Run: func(cmd *cobra.Command, args []string) {
+ opts.Command = CmdFetch
+ if opts.Verbose {
+ trace.AdjustVerbosity(1)
+ }
+ trace.Trace("fetch with", opts, args)
+ err := RunFetch(opts, args)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+ os.Exit(1)
+ }
+ },
+ Args: cobra.MaximumNArgs(1),
+ }
+ cmd.SetHelpFunc(reallySimpleHelp)
+ return cmd
+}
diff --git a/client/go/internal/admin/deploy/curl.go b/client/go/internal/admin/deploy/curl.go
new file mode 100644
index 00000000000..b46d4e361a9
--- /dev/null
+++ b/client/go/internal/admin/deploy/curl.go
@@ -0,0 +1,122 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/vespa-engine/vespa/client/go/internal/admin/trace"
+ "github.com/vespa-engine/vespa/client/go/internal/curl"
+ "github.com/vespa-engine/vespa/client/go/internal/util"
+ "github.com/vespa-engine/vespa/client/go/internal/vespa"
+)
+
+func curlPutNothing(url string) (string, error) {
+ cmd := newCurlCommand(url, curlPutArgs())
+ cmd.Method = "PUT"
+ var out bytes.Buffer
+ err := runCurl(cmd, &out)
+ return out.String(), err
+}
+
+func curlPost(url string, input io.Reader) (string, error) {
+ cmd := newCurlCommand(url, curlPostArgs())
+ cmd.Method = "POST"
+ cmd.Header("Content-Type", "application/x-gzip")
+ cmd.WithBodyInput(input)
+ var out bytes.Buffer
+ err := runCurl(cmd, &out)
+ return out.String(), err
+}
+
+func curlPostZip(url string, input io.Reader) (string, error) {
+ cmd := newCurlCommand(url, curlPostArgs())
+ cmd.Method = "POST"
+ cmd.Header("Content-Type", "application/zip")
+ cmd.WithBodyInput(input)
+ var out bytes.Buffer
+ err := runCurl(cmd, &out)
+ return out.String(), err
+}
+
+func curlGet(url string, output io.Writer) error {
+ cmd := newCurlCommand(url, commonCurlArgs())
+ err := runCurl(cmd, output)
+ return err
+}
+
+func urlWithoutQuery(url string) string {
+ parts := strings.Split(url, "?")
+ return parts[0]
+}
+
+func newCurlCommand(url string, args []string) *curl.Command {
+ tls, err := vespa.LoadTlsConfig()
+ if err != nil {
+ util.JustExitWith(err)
+ }
+ if tls != nil && strings.HasPrefix(url, "http:") {
+ url = "https:" + url[5:]
+ }
+ cmd, err := curl.RawArgs(url, args...)
+ if err != nil {
+ util.JustExitWith(err)
+ }
+ if tls != nil {
+ if tls.DisableHostnameValidation {
+ cmd, err = curl.RawArgs(url, append(args, "--insecure")...)
+ if err != nil {
+ util.JustExitWith(err)
+ }
+ }
+ cmd.PrivateKey = tls.Files.PrivateKey
+ cmd.Certificate = tls.Files.Certificates
+ cmd.CaCertificate = tls.Files.CaCertificates
+ }
+ return cmd
+}
+
+func runCurl(cmd *curl.Command, stdout io.Writer) error {
+ trace.Trace("running curl:", cmd.String())
+ err := cmd.Run(stdout, os.Stderr)
+ if err != nil {
+ if ee, ok := err.(*exec.ExitError); ok {
+ if ee.ProcessState.ExitCode() == 7 {
+ return fmt.Errorf("HTTP request failed. Could not connect to %s", cmd.GetUrlPrefix())
+ }
+ }
+ return fmt.Errorf("HTTP request failed with curl %s", err.Error())
+ }
+ return err
+}
+
+func commonCurlArgs() []string {
+ return []string{
+ "-A", "vespa-deploy",
+ "--silent",
+ "--show-error",
+ "--connect-timeout", "30",
+ "--max-time", "1200",
+ }
+}
+
+func curlPutArgs() []string {
+ return append(commonCurlArgs(),
+ "--write-out", "\n%{http_code}")
+}
+
+func curlGetArgs() []string {
+ return commonCurlArgs()
+}
+
+func curlPostArgs() []string {
+ return append(commonCurlArgs(),
+ "--write-out", "\n%{http_code}")
+}
diff --git a/client/go/internal/admin/deploy/fetch.go b/client/go/internal/admin/deploy/fetch.go
new file mode 100644
index 00000000000..47eeb8631c6
--- /dev/null
+++ b/client/go/internal/admin/deploy/fetch.go
@@ -0,0 +1,96 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/vespa-engine/vespa/client/go/internal/util"
+)
+
+// main entry point for vespa-deploy fetch
+
+func RunFetch(opts *Options, args []string) error {
+ dirName := "."
+ if len(args) > 0 {
+ dirName = args[0]
+ }
+ src := makeConfigsourceUrl(opts)
+ url := src +
+ "/application/v2" +
+ "/tenant/" + opts.Tenant +
+ "/application/" + opts.Application +
+ "/environment/" + opts.Environment +
+ "/region/" + opts.Region +
+ "/instance/" + opts.Instance +
+ "/content/"
+
+ url = addUrlPropertyFromOption(url, strconv.Itoa(opts.Timeout), "timeout")
+ fmt.Printf("Writing active application to %s\n(using %s)\n", dirName, urlWithoutQuery(url))
+ var out bytes.Buffer
+ err := curlGet(url, &out)
+ if err != nil {
+ return err
+ }
+ fetchDirectory(dirName, &out)
+ return err
+}
+
+func fetchDirectory(name string, input *bytes.Buffer) {
+ err := os.MkdirAll(name, 0755)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+ codec := json.NewDecoder(input)
+ var result []string
+ err = codec.Decode(&result)
+ if err != nil {
+ fmt.Printf("ERROR: %v [%v] <<< %s\n", result, err, input.String())
+ return
+ }
+ for _, entry := range result {
+ fmt.Println("GET", entry)
+ fn := name + "/" + getPartAfterSlash(entry)
+ if strings.HasSuffix(entry, "/") {
+ var out bytes.Buffer
+ err := curlGet(entry, &out)
+ if err != nil {
+ fmt.Println("FAILED", err)
+ return
+ }
+ fetchDirectory(fn, &out)
+ } else {
+ f, err := os.Create(fn)
+ if err != nil {
+ fmt.Println("FAILED", err)
+ return
+ }
+ defer f.Close()
+ err = curlGet(entry, f)
+ if err != nil {
+ fmt.Println("FAILED", err)
+ return
+ }
+ }
+ }
+}
+
+func getPartAfterSlash(path string) string {
+ parts := strings.Split(path, "/")
+ idx := len(parts) - 1
+ if idx > 1 && parts[idx] == "" {
+ return parts[idx-1]
+ }
+ if idx == 0 {
+ util.JustExitMsg("cannot find part after slash: " + path)
+ }
+ return parts[idx]
+}
diff --git a/client/go/internal/admin/deploy/options.go b/client/go/internal/admin/deploy/options.go
new file mode 100644
index 00000000000..21ee8c902ed
--- /dev/null
+++ b/client/go/internal/admin/deploy/options.go
@@ -0,0 +1,70 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "strconv"
+ "strings"
+)
+
+type CmdType int
+
+const (
+ CmdNone CmdType = iota
+ CmdUpload
+ CmdPrepare
+ CmdActivate
+ CmdFetch
+)
+
+type Options struct {
+ Command CmdType
+
+ Verbose bool
+ DryRun bool
+ Force bool
+ Hosted bool
+
+ Application string
+ Environment string
+ From string
+ Instance string
+ Region string
+ Rotations string
+ ServerHost string
+ Tenant string
+ VespaVersion string
+
+ Timeout int
+ PortNumber int
+}
+
+func (opts *Options) String() string {
+ var buf strings.Builder
+ buf.WriteString("command-line options [")
+ if opts.DryRun {
+ buf.WriteString(" dry-run")
+ }
+ if opts.Force {
+ buf.WriteString(" force")
+ }
+ if opts.Hosted {
+ buf.WriteString(" hosted")
+ }
+ if opts.ServerHost != "" {
+ buf.WriteString(" server=")
+ buf.WriteString(opts.ServerHost)
+ }
+ if opts.PortNumber != 19071 {
+ buf.WriteString(" port=")
+ buf.WriteString(strconv.Itoa(opts.PortNumber))
+ }
+ if opts.From != "" {
+ buf.WriteString(" from=")
+ buf.WriteString(opts.From)
+ }
+ buf.WriteString(" ]")
+ return buf.String()
+}
diff --git a/client/go/internal/admin/deploy/persist.go b/client/go/internal/admin/deploy/persist.go
new file mode 100644
index 00000000000..e52642693fb
--- /dev/null
+++ b/client/go/internal/admin/deploy/persist.go
@@ -0,0 +1,85 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "os"
+ "path/filepath"
+
+ "github.com/vespa-engine/vespa/client/go/internal/admin/trace"
+ "github.com/vespa-engine/vespa/client/go/internal/util"
+)
+
+const (
+ cloudconfigDir = ".cloudconfig"
+ configsourceUrlUsedFileName = "deploy-configsource-url-used"
+ sessionIdFileName = "deploy-session-id"
+)
+
+func createCloudconfigDir() (string, error) {
+ userHome, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ home := filepath.Join(userHome, cloudconfigDir)
+ if err := os.MkdirAll(home, 0700); err != nil {
+ return "", err
+ }
+ return home, nil
+}
+
+func configsourceUrlUsedFile() string {
+ home, err := createCloudconfigDir()
+ if err != nil {
+ home = "/tmp"
+ }
+ return filepath.Join(home, configsourceUrlUsedFileName)
+}
+
+func createTenantDir(tenant string) string {
+ home, err := createCloudconfigDir()
+ if err != nil {
+ util.JustExitWith(err)
+ }
+ tdir := filepath.Join(home, tenant)
+ if err := os.MkdirAll(tdir, 0700); err != nil {
+ util.JustExitWith(err)
+ }
+ return tdir
+}
+
+func writeConfigsourceUrlUsed(url string) {
+ fn := configsourceUrlUsedFile()
+ os.WriteFile(fn, []byte(url), 0600)
+}
+
+func getConfigsourceUrlUsed() string {
+ fn := configsourceUrlUsedFile()
+ bytes, err := os.ReadFile(fn)
+ if err != nil {
+ return ""
+ }
+ return string(bytes)
+}
+
+func writeSessionIdToFile(tenant, newSessionId string) {
+ if newSessionId != "" {
+ dir := createTenantDir(tenant)
+ fn := filepath.Join(dir, sessionIdFileName)
+ os.WriteFile(fn, []byte(newSessionId), 0600)
+ trace.Trace("wrote", newSessionId, "to", fn)
+ }
+}
+
+func getSessionIdFromFile(tenant string) string {
+ dir := createTenantDir(tenant)
+ fn := filepath.Join(dir, sessionIdFileName)
+ bytes, err := os.ReadFile(fn)
+ if err != nil {
+ util.JustExitMsg("Could not read session id from file, and no session id supplied as argument. Exiting.")
+ }
+ trace.Trace("Session-id", string(bytes), "found from file", fn)
+ return string(bytes)
+}
diff --git a/client/go/internal/admin/deploy/prepare.go b/client/go/internal/admin/deploy/prepare.go
new file mode 100644
index 00000000000..4e048883746
--- /dev/null
+++ b/client/go/internal/admin/deploy/prepare.go
@@ -0,0 +1,83 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+)
+
+// main entry point for vespa-deploy prepare
+
+func looksLikeNumber(s string) bool {
+ var i, j int
+ n, err := fmt.Sscanf(s+" 123", "%d %d", &i, &j)
+ return n == 2 && err == nil
+}
+
+func RunPrepare(opts *Options, args []string) (err error) {
+ var response string
+ if len(args) == 0 {
+ // prepare last upload
+ sessId := getSessionIdFromFile(opts.Tenant)
+ response, err = doPrepare(opts, sessId)
+ } else if isFileOrDir(args[0]) {
+ err := RunUpload(opts, args)
+ if err != nil {
+ return err
+ }
+ return RunPrepare(opts, []string{})
+ } else if looksLikeNumber(args[0]) {
+ response, err = doPrepare(opts, args[0])
+ } else {
+ err = fmt.Errorf("Command failed. No directory or zip file found: '%s'", args[0])
+ }
+ if err != nil {
+ return err
+ }
+ var result PrepareResult
+ code, err := decodeResponse(response, &result)
+ if err != nil {
+ return err
+ }
+ for _, entry := range result.Log {
+ fmt.Println(entry.Level+":", entry.Message)
+ }
+ if code != 200 {
+ return fmt.Errorf("Request failed. HTTP status code: %d\n%s", code, result.Message)
+ }
+ fmt.Println(result.Message)
+ return err
+}
+
+func isFileOrDir(name string) bool {
+ f, err := os.Open(name)
+ if err != nil {
+ return false
+ }
+ st, err := f.Stat()
+ if err != nil {
+ return false
+ }
+ return st.Mode().IsRegular() || st.Mode().IsDir()
+}
+
+func doPrepare(opts *Options, sessionId string) (output string, err error) {
+ src := makeConfigsourceUrl(opts)
+ url := src + pathPrefix(opts) + "/" + sessionId + "/prepared"
+ url = addUrlPropertyFromFlag(url, opts.Force, "ignoreValidationErrors")
+ url = addUrlPropertyFromFlag(url, opts.DryRun, "dryRun")
+ url = addUrlPropertyFromFlag(url, opts.Verbose, "verbose")
+ url = addUrlPropertyFromFlag(url, opts.Hosted, "hostedVespa")
+ url = addUrlPropertyFromOption(url, opts.Application, "applicationName")
+ url = addUrlPropertyFromOption(url, opts.Instance, "instance")
+ url = addUrlPropertyFromOption(url, strconv.Itoa(opts.Timeout), "timeout")
+ url = addUrlPropertyFromOption(url, opts.Rotations, "rotations")
+ url = addUrlPropertyFromOption(url, opts.VespaVersion, "vespaVersion")
+ fmt.Printf("Preparing session %s using %s\n", sessionId, urlWithoutQuery(url))
+ output, err = curlPutNothing(url)
+ return
+}
diff --git a/client/go/internal/admin/deploy/results.go b/client/go/internal/admin/deploy/results.go
new file mode 100644
index 00000000000..47a05e45ab7
--- /dev/null
+++ b/client/go/internal/admin/deploy/results.go
@@ -0,0 +1,86 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "encoding/json"
+ "strings"
+)
+
+func decodeResponse(response string, v interface{}) (code int, err error) {
+ codec := json.NewDecoder(strings.NewReader(response))
+ err = codec.Decode(v)
+ if err != nil {
+ return
+ }
+ err = codec.Decode(&code)
+ return
+}
+
+type UploadResult struct {
+ Log []struct {
+ Time int64 `json:"time"`
+ Level string `json:"level"`
+ Message string `json:"message"`
+ ApplicationPackage bool `json:"applicationPackage"`
+ } `json:"log"`
+ Tenant string `json:"tenant"`
+ SessionID string `json:"session-id"`
+ Prepared string `json:"prepared"`
+ Content string `json:"content"`
+ Message string `json:"message"`
+ ErrorCode string `json:"error-code"`
+}
+
+type PrepareResult struct {
+ Log []struct {
+ Time int64 `json:"time"`
+ Level string `json:"level"`
+ Message string `json:"message"`
+ ApplicationPackage bool `json:"applicationPackage"`
+ } `json:"log"`
+ Tenant string `json:"tenant"`
+ SessionID string `json:"session-id"`
+ Activate string `json:"activate"`
+ Message string `json:"message"`
+ ErrorCode string `json:"error-code"`
+ /* not used at the moment:
+ ConfigChangeActions struct {
+ Restart []struct {
+ ClusterName string `json:"clusterName"`
+ ClusterType string `json:"clusterType"`
+ ServiceType string `json:"serviceType"`
+ Messages []string `json:"messages"`
+ Services []struct {
+ ServiceName string `json:"serviceName"`
+ ServiceType string `json:"serviceType"`
+ ConfigID string `json:"configId"`
+ HostName string `json:"hostName"`
+ } `json:"services"`
+ } `json:"restart"`
+ Refeed []interface{} `json:"refeed"`
+ Reindex []interface{} `json:"reindex"`
+ } `json:"configChangeActions"`
+ */
+}
+
+type ActivateResult struct {
+ Deploy struct {
+ From string `json:"from"`
+ Timestamp int64 `json:"timestamp"`
+ InternalRedeploy bool `json:"internalRedeploy"`
+ } `json:"deploy"`
+ Application struct {
+ ID string `json:"id"`
+ Checksum string `json:"checksum"`
+ Generation int `json:"generation"`
+ PreviousActiveGeneration int `json:"previousActiveGeneration"`
+ } `json:"application"`
+ Tenant string `json:"tenant"`
+ SessionID string `json:"session-id"`
+ Message string `json:"message"`
+ URL string `json:"url"`
+ ErrorCode string `json:"error-code"`
+}
diff --git a/client/go/internal/admin/deploy/upload.go b/client/go/internal/admin/deploy/upload.go
new file mode 100644
index 00000000000..9e963338bf7
--- /dev/null
+++ b/client/go/internal/admin/deploy/upload.go
@@ -0,0 +1,126 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/vespa-engine/vespa/client/go/internal/admin/trace"
+)
+
+// main entry point for vespa-deploy upload
+
+func RunUpload(opts *Options, args []string) error {
+ output, err := doUpload(opts, args)
+ if err != nil {
+ return err
+ }
+ var result UploadResult
+ code, err := decodeResponse(output, &result)
+ if err != nil {
+ return err
+ }
+ if code != 200 {
+ return fmt.Errorf("Request failed. HTTP status code: %d\n%s", code, result.Message)
+ }
+ fmt.Println(result.Message)
+ writeSessionIdToFile(opts.Tenant, result.SessionID)
+ return nil
+}
+
+func doUpload(opts *Options, args []string) (result string, err error) {
+ sources := makeConfigsourceUrls(opts)
+ for idx, src := range sources {
+ if idx > 0 {
+ fmt.Println(err)
+ fmt.Println("Retrying with another config server")
+ }
+ result, err = uploadToConfigSource(opts, src, args)
+ if err == nil {
+ writeConfigsourceUrlUsed(src)
+ return
+ }
+ }
+ return
+}
+
+func uploadToConfigSource(opts *Options, src string, args []string) (string, error) {
+ if opts.From != "" {
+ return uploadFrom(opts, src)
+ }
+ if len(args) == 0 {
+ return uploadDirectory(opts, src, ".")
+ } else {
+ f, err := os.Open(args[0])
+ if err != nil {
+ return "", fmt.Errorf("Command failed. No such directory found: '%s'", args[0])
+ }
+ defer f.Close()
+ st, err := f.Stat()
+ if err != nil {
+ return "", err
+ }
+ if st.Mode().IsRegular() {
+ if !strings.HasSuffix(args[0], ".zip") {
+ return "", fmt.Errorf("Application must be a zip file, was '%s'", args[0])
+ }
+ return uploadFile(opts, src, f, args[0])
+ }
+ if st.Mode().IsDir() {
+ return uploadDirectory(opts, src, args[0])
+ }
+ return "", fmt.Errorf("Bad arg '%s' with FileMode %v", args[0], st.Mode())
+ }
+}
+
+func uploadFrom(opts *Options, src string) (string, error) {
+ url := src + pathPrefix(opts)
+ url = addUrlPropertyFromOption(url, opts.From, "from")
+ url = addUrlPropertyFromFlag(url, opts.Verbose, "verbose")
+ trace.Trace("Upload from URL", opts.From, "using", urlWithoutQuery(url))
+ output, err := curlPost(url, nil)
+ return output, err
+}
+
+func uploadFile(opts *Options, src string, f *os.File, fileName string) (string, error) {
+ url := src + pathPrefix(opts)
+ url = addUrlPropertyFromFlag(url, opts.Verbose, "verbose")
+ fmt.Printf("Uploading application '%s' using %s\n", fileName, urlWithoutQuery(url))
+ output, err := curlPostZip(url, f)
+ return output, err
+}
+
+func uploadDirectory(opts *Options, src string, dirName string) (string, error) {
+ url := src + pathPrefix(opts)
+ url = addUrlPropertyFromFlag(url, opts.Verbose, "verbose")
+ fmt.Printf("Uploading application '%s' using %s\n", dirName, urlWithoutQuery(url))
+ tarCmd := tarCommand(dirName)
+ pipe, err := tarCmd.StdoutPipe()
+ if err != nil {
+ return "", err
+ }
+ err = tarCmd.Start()
+ if err != nil {
+ return "", err
+ }
+ output, err := curlPost(url, pipe)
+ tarCmd.Wait()
+ return output, err
+}
+
+func tarCommand(dirName string) *exec.Cmd {
+ args := []string{
+ "-C", dirName,
+ "--dereference",
+ "--exclude=.[a-zA-Z0-9]*",
+ "--exclude=ext",
+ "-czf", "-",
+ ".",
+ }
+ return exec.Command("tar", args...)
+}
diff --git a/client/go/internal/admin/deploy/urls.go b/client/go/internal/admin/deploy/urls.go
new file mode 100644
index 00000000000..ff43bbe29d5
--- /dev/null
+++ b/client/go/internal/admin/deploy/urls.go
@@ -0,0 +1,73 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// vespa-deploy command
+// Author: arnej
+
+package deploy
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/vespa-engine/vespa/client/go/internal/admin/trace"
+ "github.com/vespa-engine/vespa/client/go/internal/util"
+ "github.com/vespa-engine/vespa/client/go/internal/vespa"
+)
+
+func makeConfigsourceUrl(opts *Options) string {
+ src := makeConfigsourceUrls(opts)[0]
+ if opts.Command == CmdPrepare || opts.Command == CmdActivate {
+ if lastUsed := getConfigsourceUrlUsed(); lastUsed != "" {
+ return lastUsed
+ }
+ fmt.Printf("Could not read config server URL used for previous upload of an application package, trying to use %s\n", src)
+ }
+ return src
+}
+
+func makeConfigsourceUrls(opts *Options) []string {
+ var results = make([]string, 0, 3)
+ if opts.ServerHost == "" {
+ home := vespa.FindHome()
+ backticks := util.BackTicksForwardStderr
+ configsources, _ := backticks.Run(home+"/bin/vespa-print-default", "configservers_http")
+ for _, src := range strings.Split(configsources, "\n") {
+ colonParts := strings.Split(src, ":")
+ if len(colonParts) > 1 {
+ // XXX overwrites port number from above - is this sensible?
+ src = fmt.Sprintf("%s:%s:%d", colonParts[0], colonParts[1], opts.PortNumber)
+ trace.Trace("can use config server at", src)
+ results = append(results, src)
+ }
+ }
+ if len(results) == 0 {
+ trace.Warning("Could not get url to config server, make sure that VESPA_CONFIGSERVERS is set")
+ results = append(results, fmt.Sprintf("http://localhost:%d", opts.PortNumber))
+ }
+ } else {
+ results = append(results, fmt.Sprintf("http://%s:%d", opts.ServerHost, opts.PortNumber))
+ }
+ return results
+}
+
+func pathPrefix(opts *Options) string {
+ return "/application/v2/tenant/" + opts.Tenant + "/session"
+}
+
+func addUrlPropertyFromFlag(url string, flag bool, propName string) string {
+ if !flag {
+ return url
+ } else {
+ return addUrlPropertyFromOption(url, "true", propName)
+ }
+}
+
+func addUrlPropertyFromOption(url, flag, propName string) string {
+ if flag == "" {
+ return url
+ }
+ if strings.Contains(url, "?") {
+ return url + "&" + propName + "=" + flag
+ } else {
+ return url + "?" + propName + "=" + flag
+ }
+}
diff --git a/client/go/internal/admin/script-utils/main.go b/client/go/internal/admin/script-utils/main.go
index b0bd260d082..78948908a64 100644
--- a/client/go/internal/admin/script-utils/main.go
+++ b/client/go/internal/admin/script-utils/main.go
@@ -9,13 +9,13 @@ import (
"strings"
"github.com/vespa-engine/vespa/client/go/internal/admin/clusterstate"
+ "github.com/vespa-engine/vespa/client/go/internal/admin/deploy"
"github.com/vespa-engine/vespa/client/go/internal/admin/jvm"
"github.com/vespa-engine/vespa/client/go/internal/admin/script-utils/configserver"
"github.com/vespa-engine/vespa/client/go/internal/admin/script-utils/logfmt"
"github.com/vespa-engine/vespa/client/go/internal/admin/script-utils/services"
"github.com/vespa-engine/vespa/client/go/internal/admin/script-utils/standalone"
"github.com/vespa-engine/vespa/client/go/internal/admin/script-utils/startcbinary"
- "github.com/vespa-engine/vespa/client/go/internal/cli/cmd/deploy"
"github.com/vespa-engine/vespa/client/go/internal/util"
"github.com/vespa-engine/vespa/client/go/internal/vespa"
)