Repository: istio/fortio (go)

Issue:
add sha and/or date to long version and debug url
because I change the version ahead of release - when the perf setup pulls latest for instance it's not 100% clear which version is running and if it's been built clean or not etc


Pull request:
Adding sha and proper dirty/pre label depending on git tag etc
Fixes #135

dev build (go install .) look like this:
```
$ fortio version 
0.7.0-pre-unknown
$ fortio server
Fortio 0.7.0-pre echo server listening on port :8080
...
```

docker/release when dirty repo:
```
$ docker run istio/fortio:webtest version
0.7.0-pre-180218_2300_1dbde2b225b4c1bfdc6f051e782199e03414f8bd-dirty
```

clean repo but tag not matching
```
$ docker run istio/fortio:webtest version
0.7.0-pre-180218_2315_dea13ce8dc8c8ba3781d3a041ceb60a89af7ee24
$ docker run istio/fortio:webtest version -s
0.7.0-pre
```

releases should be `0.7.0` and full version `0.7.0-date_time_sha`


Patch:
diff --git a/.gitignore b/.gitignore
index 7619da2..f91ba06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ coverage.txt
 test.profile.*
 # binary, in case of go build .
 fortio
+.DS_Store
diff --git a/Dockerfile b/Dockerfile
index 151cdb6..c4e1576 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,10 @@ RUN make -C fortio submodule
 # Demonstrate moving the static directory outside of the go source tree and
 # the default data directory to a /var/lib/... volume
 RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags \
-  '-X istio.io/fortio/ui.resourcesDir=/usr/local/lib/fortio -X main.defaultDataDir=/var/lib/istio/fortio -s' \
+  "-s -X istio.io/fortio/ui.resourcesDir=/usr/local/lib/fortio -X main.defaultDataDir=/var/lib/istio/fortio \
+  -X istio.io/fortio/version.buildInfo=$(date +%y%m%d_%H%M_)$(cd fortio; git rev-parse HEAD) \
+  -X istio.io/fortio/version.tag=$(cd fortio; git describe --tags) \
+  -X istio.io/fortio/version.gitstatus=$(cd fortio; git status --porcelain | wc -l)" \
   -o fortio.bin istio.io/fortio
 # Just check it stays compiling on Windows (would need to set the rsrcDir too)
 RUN CGO_ENABLED=0 GOOS=windows go build -a -o fortio.exe istio.io/fortio
diff --git a/Makefile b/Makefile
index dd5a3e3..d925766 100755
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,8 @@
 # Makefile to build fortio's docker images as well as short cut
 # for local test/install
+#
+# See also release/README.md
+#
 
 IMAGES=echosrv # plus the combo image / Dockerfile without ext.
 
diff --git a/fhttp/http.go b/fhttp/http.go
index 0988d90..825da0d 100644
--- a/fhttp/http.go
+++ b/fhttp/http.go
@@ -34,8 +34,8 @@ import (
 
 	"istio.io/fortio/fnet"
 	"istio.io/fortio/log"
-	"istio.io/fortio/periodic"
 	"istio.io/fortio/stats"
+	"istio.io/fortio/version"
 )
 
 // Fetcher is the Url content fetcher that the different client implements.
@@ -112,10 +112,11 @@ func (h *HTTPOptions) URLSchemeCheck() {
 	}
 }
 
-// Version is the fortio package version (TODO:auto gen/extract).
+var userAgent = "istio/fortio-" + version.Short()
+
 const (
-	userAgent                  = "istio/fortio-" + periodic.Version
-	retcodeOffset              = len("HTTP/1.X ")
+	retcodeOffset = len("HTTP/1.X ")
+	// HTTPReqTimeOutDefaultValue is the default timeout value. 15s.
 	HTTPReqTimeOutDefaultValue = 15 * time.Second
 )
 
@@ -1098,7 +1099,7 @@ func DebugHandler(w http.ResponseWriter, r *http.Request) {
 	log.LogVf("%v %v %v %v", r.Method, r.URL, r.Proto, r.RemoteAddr)
 	var buf bytes.Buffer
 	buf.WriteString("Φορτίο version ")
-	buf.WriteString(periodic.Version)
+	buf.WriteString(version.Long())
 	buf.WriteString(" echo debug server up for ")
 	buf.WriteString(fmt.Sprint(RoundDuration(time.Since(startTime))))
 	buf.WriteString(" on ")
@@ -1157,7 +1158,7 @@ func DebugHandler(w http.ResponseWriter, r *http.Request) {
 func Serve(port, debugPath string) {
 	startTime = time.Now()
 	nPort := fnet.NormalizePort(port)
-	fmt.Printf("Fortio %s echo server listening on port %s\n", periodic.Version, nPort)
+	fmt.Printf("Fortio %s echo server listening on port %s\n", version.Short(), nPort)
 	if debugPath != "" {
 		http.HandleFunc(debugPath, DebugHandler)
 	}
diff --git a/fortio_main.go b/fortio_main.go
index 73514c0..865ae72 100644
--- a/fortio_main.go
+++ b/fortio_main.go
@@ -32,6 +32,7 @@ import (
 	"istio.io/fortio/periodic"
 	"istio.io/fortio/stats"
 	"istio.io/fortio/ui"
+	"istio.io/fortio/version"
 )
 
 var httpOpts fhttp.HTTPOptions
@@ -54,7 +55,7 @@ func (f *flagList) Set(value string) error {
 func usage(msgs ...interface{}) {
 	// nolint: gas
 	fmt.Fprintf(os.Stderr, "Φορτίο %s usage:\n\t%s command [flags] target\n%s\n%s\n%s\n%s\n%s\n",
-		periodic.Version,
+		version.Short(),
 		os.Args[0],
 		"where command is one of: load (load testing), server (starts grpc ping and",
 		"http echo/ui/redirect servers), grpcping (grpc client), report (report only UI",
@@ -122,8 +123,13 @@ func main() {
 	flag.BoolVar(&fhttp.CheckConnectionClosedHeader, "httpccch", fhttp.CheckConnectionClosedHeader,
 		"Check for Connection: Close Header")
 	// Special case so `fortio -version` and `--version` and `version` and ... work
-	if len(os.Args) == 2 && strings.Contains(os.Args[1], "version") {
-		fmt.Println(periodic.Version)
+	if len(os.Args) >= 2 && strings.Contains(os.Args[1], "version") {
+		if len(os.Args) >= 3 && strings.Contains(os.Args[2], "s") {
+			// so `fortio version -s` is the short version; everything else is long/full
+			fmt.Println(version.Short())
+		} else {
+			fmt.Println(version.Long())
+		}
 		os.Exit(0)
 	}
 	if len(os.Args) < 2 {
@@ -208,7 +214,7 @@ func fortioLoad(justCurl bool) {
 	out := os.Stderr
 	qps := *qpsFlag // TODO possibly use translated <=0 to "max" from results/options normalization in periodic/
 	fmt.Fprintf(out, "Fortio %s running at %g queries per second, %d->%d procs",
-		periodic.Version, qps, prevGoMaxProcs, runtime.GOMAXPROCS(0))
+		version.Short(), qps, prevGoMaxProcs, runtime.GOMAXPROCS(0))
 	if *exactlyFlag > 0 {
 		fmt.Fprintf(out, ", for %d calls: %s\n", *exactlyFlag, url)
 	} else {
diff --git a/periodic/periodic.go b/periodic/periodic.go
index 1d46775..0f6f3ba 100644
--- a/periodic/periodic.go
+++ b/periodic/periodic.go
@@ -33,11 +33,7 @@ import (
 
 	"istio.io/fortio/log"
 	"istio.io/fortio/stats"
-)
-
-const (
-	// Version is the overall package version (used to version json output too).
-	Version = "0.6.9"
+	"istio.io/fortio/version"
 )
 
 // DefaultRunnerOptions are the default values for options (do not mutate!).
@@ -432,7 +428,7 @@ func (r *periodicRunner) Run() RunnerResults {
 		requestedDuration += fmt.Sprintf(", interrupted after %d", actualCount)
 	}
 	result := RunnerResults{r.Labels, start, requestedQPS, requestedDuration,
-		actualQPS, elapsed, r.NumThreads, Version, functionDuration.Export().CalcPercentiles(r.Percentiles), r.Exactly}
+		actualQPS, elapsed, r.NumThreads, version.Short(), functionDuration.Export().CalcPercentiles(r.Percentiles), r.Exactly}
 	if log.Log(log.Warning) {
 		result.DurationHistogram.Print(r.Out, "Aggregated Function Time")
 	} else {
diff --git a/pingsrv.go b/pingsrv.go
index a5198b4..a9039e6 100644
--- a/pingsrv.go
+++ b/pingsrv.go
@@ -35,8 +35,8 @@ import (
 	"istio.io/fortio/fgrpc"
 	"istio.io/fortio/fnet"
 	"istio.io/fortio/log"
-	"istio.io/fortio/periodic"
 	"istio.io/fortio/stats"
+	"istio.io/fortio/version"
 )
 
 // To get most debugging/tracing:
@@ -70,7 +70,7 @@ func pingServer(port string) {
 	healthServer.SetServingStatus("ping", grpc_health_v1.HealthCheckResponse_SERVING)
 	grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
 	fgrpc.RegisterPingServerServer(grpcServer, &pingSrv{})
-	fmt.Printf("Fortio %s grpc ping server listening on port %v\n", periodic.Version, port)
+	fmt.Printf("Fortio %s grpc ping server listening on port %v\n", version.Short(), port)
 	if err := grpcServer.Serve(socket); err != nil {
 		log.Fatalf("failed to start grpc server: %v", err)
 	}
diff --git a/release/README.md b/release/README.md
index 4f1bed9..678e213 100644
--- a/release/README.md
+++ b/release/README.md
@@ -1,33 +1,38 @@
-# How to make a fortio release
+## How to make a fortio release
 
-- Make sure `periodic/periodic.go`'s `Version` is newer than https://github.com/istio/fortio/releases
+- Make sure `version/version.go`'s `major`/`minor`/`patch` is newer than https://github.com/istio/fortio/releases
 
 - Make a release there and document the changes since the previous release
 
-- Make sure to use the same tag format (e.g "0.4.3" - note that there are no "v" in the tag to be consistent with the rest istio)
+- Make sure to use the same git tag format (e.g "v0.7.1" - note that there is `v` prefix in the tag, like many projects but unlike the rest of istio). Docker and internal version/tag is "0.7.1", the `v` is only for git tags.
+
+- Make sure your git status is clean, and the tag is present before the next step or it will get marked dirty/pre
 
 - Create the binary tgz: `make release` (from/in the toplevel directory)
 
 - Upload the release/fortio-\*.tgz to GitHub
 
+- The docker official builds are done automatically based on tag, check https://cloud.docker.com/app/istio/repository/docker/istio/fortio/builds
+
+- Increment the `patch` and commit that right away so the first point is true next time and so master/latest docker images have the correct next-pre version.
 
 ## How to change the build image
 
 Update [../Dockerfile.build](../Dockerfile.build)
 
+Edit the `BUILD_IMAGE_TAG := v5` line in the Makefile, set it to `v6`
+for instance (replace `v6` by whichever is the next one at the time)
+
 run
 ```
-make update-build-image TAG=v5 DOCKER_PREFIX=fortio/fortio
+make update-build-image
 ```
 
-Make sure it gets successfully pushed to the fortio/fortio registry
-
-replace `v5` by whichever is the next one at the time
+Make sure it gets successfully pushed to the istio/fortio registry
 
 run
 ```
-make update-build-image-tag TAG=v5
+make update-build-image-tag
 ```
-with same TAG as before
 
 Check the diff and make lint, webtest, etc and PR
diff --git a/ui/uihandler.go b/ui/uihandler.go
index 3a4b60c..00b6167 100644
--- a/ui/uihandler.go
+++ b/ui/uihandler.go
@@ -42,6 +42,7 @@ import (
 	"istio.io/fortio/log"
 	"istio.io/fortio/periodic"
 	"istio.io/fortio/stats"
+	"istio.io/fortio/version"
 )
 
 // TODO: move some of those in their own files/package (e.g data transfer TSV)
@@ -236,7 +237,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
 			URLHostPort                 string
 			DoStop                      bool
 			DoLoad                      bool
-		}{r, opts.GetHeaders(), periodic.Version, logoPath, debugPath, chartJSPath,
+		}{r, opts.GetHeaders(), version.Short(), logoPath, debugPath, chartJSPath,
 			startTime.Format(time.ANSIC), url, labels, runid,
 			fhttp.RoundDuration(time.Since(startTime)), durSeconds, urlHostPort, mode == stop, mode == run})
 		if err != nil {
@@ -413,7 +414,7 @@ func BrowseHandler(w http.ResponseWriter, r *http.Request) {
 		URLHostPort string
 		DoRender    bool
 		DoSearch    bool
-	}{r, extraBrowseLabel, periodic.Version, logoPath, chartJSPath,
+	}{r, extraBrowseLabel, version.Short(), logoPath, chartJSPath,
 		url, search, dataList, urlHostPort, doRender, (search != "")})
 	if err != nil {
 		log.Critf("Template execution failed: %v", err)
@@ -645,7 +646,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) {
 			Version  string
 			LogoPath string
 			URL      string
-		}{periodic.Version, logoPath, uStr})
+		}{version.Short(), logoPath, uStr})
 		if err != nil {
 			log.Critf("Sync template execution failed: %v", err)
 		}
@@ -854,8 +855,8 @@ func Serve(baseurl, port, debugpath, uipath, staticRsrcDir string, datadir strin
 	http.HandleFunc(fetchPath, FetcherHandler)
 	fhttp.CheckConnectionClosedHeader = true // needed for proxy to avoid errors
 
-	logoPath = periodic.Version + "/static/img/logo.svg"
-	chartJSPath = periodic.Version + "/static/js/Chart.min.js"
+	logoPath = version.Short() + "/static/img/logo.svg"
+	chartJSPath = version.Short() + "/static/js/Chart.min.js"
 
 	// Serve static contents in the ui/static dir. If not otherwise specified
 	// by the function parameter staticPath, we use getResourcesDir which uses the
@@ -865,7 +866,7 @@ func Serve(baseurl, port, debugpath, uipath, staticRsrcDir string, datadir strin
 	staticRsrcDir = getResourcesDir(staticRsrcDir)
 	if staticRsrcDir != "" {
 		fs := http.FileServer(http.Dir(staticRsrcDir))
-		prefix := uiPath + periodic.Version
+		prefix := uiPath + version.Short()
 		http.Handle(prefix+"/static/", LogAndAddCacheControl(http.StripPrefix(prefix, fs)))
 		http.Handle(faviconPath, LogAndAddCacheControl(fs))
 		var err error
@@ -906,11 +907,11 @@ func Report(baseurl, port, staticRsrcDir string, datadir string) {
 	fmt.Printf(uiMsg + "\n")
 	uiPath = "/"
 	dataDir = datadir
-	logoPath = periodic.Version + "/static/img/logo.svg"
-	chartJSPath = periodic.Version + "/static/js/Chart.min.js"
+	logoPath = version.Short() + "/static/img/logo.svg"
+	chartJSPath = version.Short() + "/static/js/Chart.min.js"
 	staticRsrcDir = getResourcesDir(staticRsrcDir)
 	fs := http.FileServer(http.Dir(staticRsrcDir))
-	prefix := uiPath + periodic.Version
+	prefix := uiPath + version.Short()
 	http.Handle(prefix+"/static/", LogAndAddCacheControl(http.StripPrefix(prefix, fs)))
 	http.Handle(faviconPath, LogAndAddCacheControl(fs))
 	var err error
diff --git a/version/version.go b/version/version.go
new file mode 100644
index 0000000..c728a24
--- /dev/null
+++ b/version/version.go
@@ -0,0 +1,87 @@
+// Copyright 2017 Istio Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package version for fortio holds version information and build information.
+package version // import "istio.io/fortio/version"
+import (
+	"fmt"
+
+	"istio.io/fortio/log"
+)
+
+const (
+	major = 0
+	minor = 7
+	patch = 0
+
+	debug = false // turn on to debug init()
+)
+
+var (
+	// The following are set by Dockerfile during link time:
+	tag       = "pre"
+	buildInfo = "unknown"
+	// Number of lines in git status --porcelain; 0 means clean
+	gitstatus = "0" // buildInfo default is unknown so no need to add -dirty
+	// computed in init()
+	version     = ""
+	longVersion = ""
+)
+
+// Major returns the numerical major version number (first digit of Version()).
+func Major() int {
+	return major
+}
+
+// Minor returns the numerical minor version number (second digit of Version()).
+func Minor() int {
+	return minor
+}
+
+// Patch returns the numerical patch level (third digit of Version()).
+func Patch() int {
+	return patch
+}
+
+// Short returns the 3 digit short version string Major.Minor.Patch[-build]
+// version.Short() is the overall project version (used to version json
+// output too).
+func Short() string {
+	return version
+}
+
+// Long returns the full version and build information.
+func Long() string {
+	return longVersion
+}
+
+// Carefully manually tested all the combinations in pair with Dockerfile
+func init() {
+	if debug {
+		log.SetLogLevel(log.Debug)
+	}
+	version = fmt.Sprintf("%d.%d.%d", major, minor, patch)
+	clean := (gitstatus == "0")
+	// The docker build will pass the git tag to the build, if it is clean
+	// from a tag it will look like v0.7.0
+	if tag != "v"+version || !clean {
+		log.Debugf("tag is %v, clean is %v marking as pre release", tag, clean)
+		version += "-pre"
+	}
+	if !clean {
+		buildInfo += "-dirty"
+		log.Debugf("gitstatus is %q, marking buildinfo as dirty: %v", gitstatus, buildInfo)
+	}
+	longVersion = version + "-" + buildInfo
+}


Test patch:
diff --git a/Webtest.sh b/Webtest.sh
index 61927bb..8ce9a27 100755
--- a/Webtest.sh
+++ b/Webtest.sh
@@ -24,7 +24,7 @@ $CURL ${BASE_FORTIO}browse
 # Check we can connect, and run a QPS test against ourselves through fetch
 $CURL "${BASE_FORTIO}fetch/localhost:8080$FORTIO_UI_PREFIX?url=http://localhost:8080/debug&load=Start&qps=-1&json=on" | grep ActualQPS
 # Check we get the logo (need to remove the CR from raw headers)
-VERSION=$(docker exec fortio_server /usr/local/bin/fortio -version)
+VERSION=$(docker exec fortio_server /usr/local/bin/fortio version -s)
 LOGO_TYPE=$($CURL "${BASE_FORTIO}${VERSION}/static/img/logo.svg" | grep -i Content-Type: | tr -d '\r'| awk '{print $2}')
 if [ "$LOGO_TYPE" != "image/svg+xml" ]; then
   echo "Unexpected content type for the logo: $LOGO_TYPE"
diff --git a/fgrpc/grpcrunner_test.go b/fgrpc/grpcrunner_test.go
index 65d1100..d8b1472 100644
--- a/fgrpc/grpcrunner_test.go
+++ b/fgrpc/grpcrunner_test.go
@@ -26,6 +26,7 @@ import (
 
 	"istio.io/fortio/log"
 	"istio.io/fortio/periodic"
+	"istio.io/fortio/version"
 
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/health"
@@ -44,7 +45,7 @@ func DynamicGRPCHealthServer() int {
 	healthServer := health.NewServer()
 	healthServer.SetServingStatus("ping", grpc_health_v1.HealthCheckResponse_SERVING)
 	grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
-	fmt.Printf("Fortio %s grpc health server listening on port %v\n", periodic.Version, addr)
+	fmt.Printf("Fortio %s grpc health server listening on port %v\n", version.Short(), addr)
 	go func(socket net.Listener) {
 		if e := grpcServer.Serve(socket); e != nil {
 			log.Fatalf("failed to start grpc server: %v", e)