diff options
author | Arne H Juul <arnej27959@users.noreply.github.com> | 2022-08-25 10:50:18 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-25 10:50:18 +0200 |
commit | 490223783062e2f95a517c37ac6b3fa47e2bc73f (patch) | |
tree | 4b8bd00e6e78c31f26f94b58409c801844b9b922 /client | |
parent | 521a6fe075a4eb895703cacd1787ccd1b4d7499f (diff) | |
parent | ba290bf93310ca1f2273c11f3f877231a850a2cc (diff) |
Merge pull request #23773 from vespa-engine/arnej/add-deploy-go-1
add go code for vespa-deploy
Diffstat (limited to 'client')
-rw-r--r-- | client/go/cmd/deploy/activate.go | 44 | ||||
-rw-r--r-- | client/go/cmd/deploy/curl.go | 114 | ||||
-rw-r--r-- | client/go/cmd/deploy/fetch.go | 94 | ||||
-rw-r--r-- | client/go/cmd/deploy/options.go | 37 | ||||
-rw-r--r-- | client/go/cmd/deploy/persist.go | 105 | ||||
-rw-r--r-- | client/go/cmd/deploy/prepare.go | 80 | ||||
-rw-r--r-- | client/go/cmd/deploy/results.go | 86 | ||||
-rw-r--r-- | client/go/cmd/deploy/upload.go | 125 | ||||
-rw-r--r-- | client/go/cmd/deploy/urls.go | 69 | ||||
-rw-r--r-- | client/go/cmd/logfmt/runlogfmt.go | 12 | ||||
-rw-r--r-- | client/go/vespa-deploy/cmd.go | 146 | ||||
-rw-r--r-- | client/go/vespa-deploy/main.go | 10 | ||||
-rw-r--r-- | client/go/vespa/find_home.go | 60 |
13 files changed, 973 insertions, 9 deletions
diff --git a/client/go/cmd/deploy/activate.go b/client/go/cmd/deploy/activate.go new file mode 100644 index 00000000000..a6b7de81342 --- /dev/null +++ b/client/go/cmd/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 := curlPut(url, src) + 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/cmd/deploy/curl.go b/client/go/cmd/deploy/curl.go new file mode 100644 index 00000000000..67edb0ee010 --- /dev/null +++ b/client/go/cmd/deploy/curl.go @@ -0,0 +1,114 @@ +// 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/vespa" +) + +func curlPut(url string, cfgSrc string) (string, error) { + args := append(curlPutArgs(), url) + return runCurl(args, new(strings.Reader), cfgSrc) +} + +func curlPost(url string, input io.Reader, cfgSrc string) (string, error) { + args := append(curlPostArgs(), url) + return runCurl(args, input, cfgSrc) +} + +func curlPostZip(url string, input io.Reader, cfgSrc string) (string, error) { + args := append(curlPostZipArgs(), url) + return runCurl(args, input, cfgSrc) +} + +func curlGet(url string, output io.Writer) error { + args := append(curlGetArgs(), url) + cmd := exec.Command(curlWrapper(), args...) + cmd.Stdout = output + cmd.Stderr = os.Stderr + // fmt.Printf("running command: %v\n", cmd) + err := cmd.Run() + return err +} + +func urlWithoutQuery(url string) string { + before, _, _ := strings.Cut(url, "?") + return before +} + +func getOutputFromCmd(program string, args ...string) (string, error) { + cmd := exec.Command(program, args...) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = os.Stderr + err := cmd.Run() + return out.String(), err +} + +func runCurl(args []string, input io.Reader, cfgSrc string) (string, error) { + cmd := exec.Command(curlWrapper(), args...) + cmd.Stdin = input + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = os.Stderr + // fmt.Printf("running command: %v\n", cmd) + err := cmd.Run() + // fmt.Printf("output: %s\n", out.String()) + if err != nil { + if cmd.ProcessState.ExitCode() == 7 { + return "", fmt.Errorf("HTTP request failed. Could not connect to %s", cfgSrc) + } + return "", fmt.Errorf("HTTP request failed with curl %s", err.Error()) + } + return out.String(), err +} + +func curlWrapper() string { + return vespa.FindHome() + "/libexec/vespa/vespa-curl-wrapper" +} + +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", "%{http_code}", + "--request", "PUT") +} + +func curlGetArgs() []string { + return append(commonCurlArgs(), + "--request", "GET") +} + +func curlPostArgs() []string { + return append(commonCurlArgs(), + "--write-out", "%{http_code}", + "--request", "POST", + "--header", "Content-Type: application/x-gzip", + "--data-binary", "@-") +} + +func curlPostZipArgs() []string { + return append(commonCurlArgs(), + "--write-out", "%{http_code}", + "--request", "POST", + "--header", "Content-Type: application/zip", + "--data-binary", "@-") +} diff --git a/client/go/cmd/deploy/fetch.go b/client/go/cmd/deploy/fetch.go new file mode 100644 index 00000000000..3b65509c0a3 --- /dev/null +++ b/client/go/cmd/deploy/fetch.go @@ -0,0 +1,94 @@ +// 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" +) + +// 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 { + return parts[idx] + } + panic("cannot find part after slash: " + path) +} diff --git a/client/go/cmd/deploy/options.go b/client/go/cmd/deploy/options.go new file mode 100644 index 00000000000..2f71f779044 --- /dev/null +++ b/client/go/cmd/deploy/options.go @@ -0,0 +1,37 @@ +// 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 + +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 +} diff --git a/client/go/cmd/deploy/persist.go b/client/go/cmd/deploy/persist.go new file mode 100644 index 00000000000..78fe063ea0e --- /dev/null +++ b/client/go/cmd/deploy/persist.go @@ -0,0 +1,105 @@ +// 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" + "path/filepath" + "strconv" + "strings" +) + +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 { + panic(err) + } + tdir := filepath.Join(home, tenant) + if err := os.MkdirAll(tdir, 0700); err != nil { + panic(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 writeSessionIdFromResponseToFile(tenant, response string) { + newSessionId := getSessionIdFromResponse(response) + writeSessionIdToFile(tenant, newSessionId) +} + +func writeSessionIdToFile(tenant, newSessionId string) { + if newSessionId != "" { + dir := createTenantDir(tenant) + fn := filepath.Join(dir, sessionIdFileName) + os.WriteFile(fn, []byte(newSessionId), 0600) + // fmt.Printf("wrote %s to %s\n", newSessionId, fn) + } +} + +func getSessionIdFromResponse(response string) string { + _, after, found := strings.Cut(response, `"session-id":"`) + if !found { + return "" + } + digits, _, found := strings.Cut(after, `"`) + if !found { + return "" + } + if _, err := strconv.Atoi(digits); err != nil { + return "" + } + return digits +} + +func getSessionIdFromFile(tenant string) string { + dir := createTenantDir(tenant) + fn := filepath.Join(dir, sessionIdFileName) + bytes, err := os.ReadFile(fn) + if err == nil { + // fmt.Printf("Session-id '%s' found from file %s\n", string(bytes), fn) + return string(bytes) + } + panic("Could not read session id from file, and no session id supplied as argument. Exiting.") +} diff --git a/client/go/cmd/deploy/prepare.go b/client/go/cmd/deploy/prepare.go new file mode 100644 index 00000000000..ace14c9b949 --- /dev/null +++ b/client/go/cmd/deploy/prepare.go @@ -0,0 +1,80 @@ +// 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]) { + RunUpload(opts, args) + 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 = curlPut(url, src) + return +} diff --git a/client/go/cmd/deploy/results.go b/client/go/cmd/deploy/results.go new file mode 100644 index 00000000000..47a05e45ab7 --- /dev/null +++ b/client/go/cmd/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/cmd/deploy/upload.go b/client/go/cmd/deploy/upload.go new file mode 100644 index 00000000000..57d84e9923c --- /dev/null +++ b/client/go/cmd/deploy/upload.go @@ -0,0 +1,125 @@ +// 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" +) + +// 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") + // disallowed by system test: + // fmt.Printf("Upload from URL %s using %s\n", opts.From, urlWithoutQuery(url)) + output, err := curlPost(url, nil, src) + 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, src) + 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, src) + 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/cmd/deploy/urls.go b/client/go/cmd/deploy/urls.go new file mode 100644 index 00000000000..a865006df0d --- /dev/null +++ b/client/go/cmd/deploy/urls.go @@ -0,0 +1,69 @@ +// 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/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() + configsources, _ := getOutputFromCmd(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) + results = append(results, src) + } + } + if len(results) == 0 { + fmt.Println("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/cmd/logfmt/runlogfmt.go b/client/go/cmd/logfmt/runlogfmt.go index 5abc4cc1cb8..5b9a3ac0870 100644 --- a/client/go/cmd/logfmt/runlogfmt.go +++ b/client/go/cmd/logfmt/runlogfmt.go @@ -8,6 +8,8 @@ import ( "bufio" "fmt" "os" + + "github.com/vespa-engine/vespa/client/go/vespa" ) func inputIsPipe() bool { @@ -22,20 +24,12 @@ func inputIsPipe() bool { } } -func vespaHome() string { - ev := os.Getenv("VESPA_HOME") - if ev == "" { - return "/opt/vespa" - } - return ev -} - // main entry point for vespa-logfmt func RunLogfmt(opts *Options, args []string) { if len(args) == 0 { if !inputIsPipe() { - args = append(args, vespaHome()+"/logs/vespa/vespa.log") + args = append(args, vespa.FindHome()+"/logs/vespa/vespa.log") } else { formatFile(opts, os.Stdin) } diff --git a/client/go/vespa-deploy/cmd.go b/client/go/vespa-deploy/cmd.go new file mode 100644 index 00000000000..af97dc098e5 --- /dev/null +++ b/client/go/vespa-deploy/cmd.go @@ -0,0 +1,146 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// vespa-deploy command +// Author: arnej + +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/vespa-engine/vespa/client/go/build" + "github.com/vespa-engine/vespa/client/go/cmd/deploy" +) + +func reallySimpleHelp(cmd *cobra.Command, args []string) { + fmt.Println("Usage: vespa-deploy", cmd.Use) +} + +func NewDeployCmd() *cobra.Command { + var ( + curOptions deploy.Options + ) + 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 *deploy.Options) *cobra.Command { + cmd := &cobra.Command{ + Use: "upload <application package>", + Run: func(cmd *cobra.Command, args []string) { + opts.Command = deploy.CmdUpload + if opts.Verbose { + fmt.Printf("upload %v [%v]\n", opts, args) + } + err := deploy.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 *deploy.Options) *cobra.Command { + cmd := &cobra.Command{ + Use: "prepare [<session_id> | <application package>]", + Run: func(cmd *cobra.Command, args []string) { + opts.Command = deploy.CmdPrepare + if opts.Verbose { + fmt.Printf("prepare %v [%v]\n", opts, args) + } + err := deploy.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 *deploy.Options) *cobra.Command { + cmd := &cobra.Command{ + Use: "activate [<session_id>]", + Run: func(cmd *cobra.Command, args []string) { + opts.Command = deploy.CmdActivate + if opts.Verbose { + fmt.Printf("activate %v [%v]\n", opts, args) + } + err := deploy.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 *deploy.Options) *cobra.Command { + cmd := &cobra.Command{ + Use: "fetch <output directory>", + Run: func(cmd *cobra.Command, args []string) { + opts.Command = deploy.CmdFetch + if opts.Verbose { + fmt.Printf("fetch %v [%v]\n", opts, args) + } + err := deploy.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/vespa-deploy/main.go b/client/go/vespa-deploy/main.go new file mode 100644 index 00000000000..549f5511765 --- /dev/null +++ b/client/go/vespa-deploy/main.go @@ -0,0 +1,10 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// vespa-deploy command +// Author: arnej + +package main + +func main() { + cobra := NewDeployCmd() + cobra.Execute() +} diff --git a/client/go/vespa/find_home.go b/client/go/vespa/find_home.go new file mode 100644 index 00000000000..8d613b9d8c0 --- /dev/null +++ b/client/go/vespa/find_home.go @@ -0,0 +1,60 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// get or find VESPA_HOME +// Author: arnej + +package vespa + +import ( + "os" + "strings" +) + +func FindHome() string { + const ( + defaultInstallDir = "opt/vespa" + commonEnvSh = "libexec/vespa/common-env.sh" + ) + // use env var if it is set: + if ev := os.Getenv("VESPA_HOME"); ev != "" { + return ev + } + // some helper functions... + var dirName = func(path string) string { + idx := strings.LastIndex(path, "/") + if idx < 0 { + return "" + } + return path[:idx] + } + var isFile = func(fn string) bool { + st, err := os.Stat(fn) + return err == nil && st.Mode().IsRegular() + } + var findPath = func() string { + myProgName := os.Args[0] + if strings.HasPrefix(myProgName, "/") { + return dirName(myProgName) + } + if strings.Contains(myProgName, "/") { + dir, _ := os.Getwd() + return dir + "/" + dirName(myProgName) + } + for _, dir := range strings.Split(os.Getenv("PATH"), ":") { + fn := dir + "/" + myProgName + if isFile(fn) { + return dir + } + } + return "" + } + // detect path from argv[0] + for path := findPath(); path != ""; path = dirName(path) { + if isFile(path + "/" + commonEnvSh) { + os.Setenv("VESPA_HOME", path) + return path + } + } + // fallback + os.Setenv("VESPA_HOME", defaultInstallDir) + return defaultInstallDir +} |