diff --git a/Dockerfile b/Dockerfile index 30ad5d337..9c00637e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))" # ldflags -X injects commit version into binary RUN license-check -path ./ --verbose=false \ - && go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \ + && OPEN_FAAS_TELEMETRY=0 go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \ && VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \ && GIT_COMMIT=$(git rev-list -1 HEAD) \ && CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli diff --git a/Dockerfile.redist b/Dockerfile.redist index 243c3f579..2bc7a1982 100644 --- a/Dockerfile.redist +++ b/Dockerfile.redist @@ -14,13 +14,18 @@ RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))" # ldflags -X injects commit version into binary RUN license-check -path ./ --verbose=false \ - && go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \ + && OPEN_FAAS_TELEMETRY=0 go test $(go list ./... | grep -v /vendor/ | grep -v /template/|grep -v /build/) -cover \ && VERSION=$(git describe --all --exact-match `git rev-parse HEAD` | grep tags | sed 's/tags\///') \ && GIT_COMMIT=$(git rev-list -1 HEAD) \ + && echo "Building linux..." \ && CGO_ENABLED=0 GOOS=linux go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli \ + && echo "Building darwin..." \ && CGO_ENABLED=0 GOOS=darwin go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli-darwin \ + && echo "Building windows..." \ && CGO_ENABLED=0 GOOS=windows go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli.exe \ + && echo "Building linux (arm)..." \ && CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli-armhf \ + && echo "Building linux (arm64)..." \ && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build --ldflags "-s -w -X github.com/openfaas/faas-cli/version.GitCommit=${GIT_COMMIT} -X github.com/openfaas/faas-cli/version.Version=${VERSION}" -a -installsuffix cgo -o faas-cli-arm64 FROM alpine:latest diff --git a/Makefile b/Makefile index b07ef9aa6..ab1467078 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ build_redist: ./build_redist.sh test-unit: - go test $(shell go list ./... | grep -v /vendor/ | grep -v /template/ | grep -v build) -cover + OPEN_FAAS_TELEMETRY=0 go test $(shell go list ./... | grep -v /vendor/ | grep -v /template/ | grep -v build) -cover diff --git a/README.md b/README.md index f0e90d767..386f2a76f 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,9 @@ $ uname -a | curl http://localhost:8080/function/nodejs-echo--data-binary @- > For further instructions on the manual CLI flags (without using a YAML file) read [manual_cli.md](https://github.com/openfaas/faas-cli/blob/master/MANUAL_CLI.md) +## Analytics + +The `faas-cli` now gathers anonymous aggregate analytics on its use, for details on what and how data is gathered, along with how to opt-out read [analytics.md](analytics.md) ### FaaS-CLI Developers / Contributors diff --git a/analytics.md b/analytics.md new file mode 100644 index 000000000..29fe2bb73 --- /dev/null +++ b/analytics.md @@ -0,0 +1,54 @@ +# Analytics Overview + +The OpenFaaS `faas-cli` utility has begun gathering anonymous aggregate analytics on its use and reporting these to Google Analytics. You will be notified the first time you run `faas-cli`. + +## Why? + +OpenFaaS and its CLI are provided free of charge and run entirely by community volunteers in their spare time. As a result, we do not have the resources to do detailed user studies of OpenFaaS users to decide how best to design future features and prioritise work. Anonymous aggregate user analytics allow us to prioritise fixes and features based on how, where and when people use Homebrew. + +## Where? + +The OpenFaaS project sends analytics events to Google Analytics via HTTPS. + +## What? + +The `faas-cli` currently shares the following information each time a command is invoked. + + - ProtocolVersion `v` - The Google Analytics Protocol version, currently `1`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#v + - Tracking ID `tid` - The `faas-cli` application tracking ID, e.g. `UA-107707760-2`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#tid + - Hit Type `t` - The type of analytics hit, `faas-cli` uses the `event` type. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#t + - Client ID `cid` - A `faas-cli` analytics user ID, e.g. `c1481af7-8682-462f-a586-71d752dbe87b`. This is a UUID4 generated by the [`github.com/satori/go.uuid`](https://github.com/satori/go.uuid) package and stored in the local config file `~/.openfaas/analytics_uuid`. This does not allow us to track individual users but does enable us to accurately measure user counts vs. event counts. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid + - ApplicationName `an` - The application name, e.g. `faas-cli`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#an + - ApplicationVersion `av` - The `faas-cli` version, e.g. `0.4.18d`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#av + - Anonymize IP `aip` - The IP address will be anonymized when submitting the analytics event, e.g. `1`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#aip + - Language `cd1` - A Custom Dimension containing the function language for `new`, `build` and `deploy` commands (returns `unset` for all other subcommands). + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_ + - Operating System `cd2` - A Custom Dimension containing the OS on which `faas-cli` is being run, this is determined from `runtime.GOOS`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_ + - Architecture `cd3` - A Custom Dimension containing the Architecture on which `faas-cli` is being run, this is determined from `runtime.GOOS`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cd_ +- Event Category `ec` - The `faas-cli` groups all successful CLI actions under the `cli-success` category, and failures (not yet implemented) under `cli-failure`. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ec +- Event Action `ea` - Each command invoked by the user is results in an analytics event being generated with the same name as the `faas-cli` subcommand. + - https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ea + +## How? + +The core implementation is in the `analytics` package, with calls to `analytics.Event()` each time a subcommand is invoked. + +Analytics submission occurs in a background goroutine and will fail fast to avoid delaying any execution. + +Due to the speed with which most OpenFaaS API calls return, a timeout of 300 milliseconds is used to give the analytics submission time to complete. + +## Opting out? + +If you wish to opt out of submitting analytics events you may do so by setting the `OPEN_FAAS_TELEMETRY` environment variable. + + export OPEN_FAAS_TELEMETRY=0 diff --git a/analytics/config.go b/analytics/config.go new file mode 100644 index 000000000..3ebda53bb --- /dev/null +++ b/analytics/config.go @@ -0,0 +1,77 @@ +// Copyright (c) Alex Ellis 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package analytics + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + homedir "github.com/mitchellh/go-homedir" + "github.com/satori/go.uuid" +) + +const analyticsUUIDFile = "analytics_uuid" + +func configDir() (string, error) { + h, err := homedir.Dir() + if err != nil { + return "", fmt.Errorf("unable to detect homedir: %v", err) + } + fullpath, err := homedir.Expand(h) + if err != nil { + return "", fmt.Errorf("unable to expand homedir [%s]: %v", h, err) + } + + return path.Clean(fullpath + "/.openfaas/"), nil +} + +func configFile() string { + dir, _ := configDir() + return path.Clean(dir + "/" + analyticsUUIDFile) +} + +func configDirExists() bool { + dir, _ := configDir() + if stat, err := os.Stat(dir); err == nil && stat.IsDir() { + return true + } + return false +} + +func setUUID() (string, error) { + if !configDirExists() { + dir, _ := configDir() + err := os.Mkdir(dir, 0700) + if err != nil { + return "", fmt.Errorf("Unable to create config dir: %v\n", err) + } + } + uuidFile := configFile() + uuidStr := uuid.NewV4().String() + d1 := []byte(uuidStr) + err := ioutil.WriteFile(uuidFile, d1, 0644) + if err != nil { + return "", fmt.Errorf("unable to write analytics ID file: %v", err) + } + fmt.Fprintln(os.Stderr, "# Creating analytics file in:", uuidFile) + fmt.Fprintln(os.Stderr, "# Please see https://github.com/openfaas/faas-cli/blob/master/analytics.md for more information.") + return uuidStr, nil +} + +func getUUID() (string, error) { + dat, err := ioutil.ReadFile(configFile()) + if err != nil { + return "", fmt.Errorf("Error reading file: %v", err) + } + uuidStr := string(dat) + + _, err = uuid.FromString(uuidStr) + if err != nil { + return "", fmt.Errorf("Unable to get valid UUID from file: %v", err) + } + + return uuidStr, nil +} diff --git a/analytics/google_analytics.go b/analytics/google_analytics.go new file mode 100644 index 000000000..23a4620e0 --- /dev/null +++ b/analytics/google_analytics.go @@ -0,0 +1,94 @@ +// Copyright (c) Alex Ellis 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package analytics + +import ( + "fmt" + "net/http" + "net/url" + "os" + "runtime" + + "github.com/google/go-querystring/query" + "github.com/openfaas/faas-cli/version" +) + +const gaHost = "www.google-analytics.com" +const trackingID = "UA-107707760-3" +const applicationName = "faas-cli3" + +// disableEnvvar will prevent submission of analytics events if set +const disableEnvvar = "OPEN_FAAS_TELEMETRY" + +// Event posts an analytics event to GA +func Event(action string, language string, ch chan int) { + if Disabled() { + return + } + u, err := NewSession(language) + if err != nil { + return + } + u.EventAction = action + go u.PostEvent(ch) +} + +// NewSession provides a setup UserSession struct with sane defaults +func NewSession(language string) (*UserSession, error) { + if len(language) == 0 { + language = "unset" + } + userSession := &UserSession{ + HTTPClient: http.DefaultClient, + ProtocolVersion: 1, + Type: "event", + TrackingID: trackingID, + ApplicationName: applicationName, + ApplicationVersion: version.BuildVersion(), + AnonymizeIP: 1, + Language: language, + OS: runtime.GOOS, + ARCH: runtime.GOARCH, + EventCategory: "cli-success", + } + + uuid, err := getUUID() + if err != nil { + uuid, err = setUUID() + if err != nil { + return nil, fmt.Errorf("Unable to get or set Analytics Client ID") + } + } + userSession.ClientID = uuid + + return userSession, nil +} + +// PostEvent submits the generated event to Google Analytics, it is a +// fire and forget process and ignores the response +func (u UserSession) PostEvent(ch chan int) { + v, _ := query.Values(u) + req := &http.Request{ + Method: http.MethodPost, + Host: gaHost, + URL: &url.URL{ + Host: gaHost, + Scheme: "https", + Path: "/collect", + RawQuery: v.Encode(), + }, + } + u.HTTPClient.Do(req) + ch <- 1 +} + +// Disabled returns true if the opt-out envvar has been set +// and false if it has not been set +func Disabled() bool { + val, ok := os.LookupEnv(disableEnvvar) + if ok && val == "0" { + return true + } + return false +} diff --git a/analytics/types.go b/analytics/types.go new file mode 100644 index 000000000..116d0a729 --- /dev/null +++ b/analytics/types.go @@ -0,0 +1,24 @@ +// Copyright (c) Alex Ellis 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package analytics + +import "net/http" + +// UserSession contains common Google Analytics event values and +// the client used to submit the event. +type UserSession struct { + HTTPClient *http.Client `url:"-"` + ProtocolVersion int `url:"v"` + Type string `url:"t"` + ClientID string `url:"cid"` + TrackingID string `url:"tid"` + ApplicationName string `url:"an"` + ApplicationVersion string `url:"av"` + AnonymizeIP int `url:"aip"` + Language string `url:"cd1"` + OS string `url:"cd2"` + ARCH string `url:"cd3"` + EventCategory string `url:"ec,omitempty"` + EventAction string `url:"ea,omitempty"` +} diff --git a/commands/build.go b/commands/build.go index 6619b39e9..303965d11 100644 --- a/commands/build.go +++ b/commands/build.go @@ -9,6 +9,7 @@ import ( "os" "sync" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/builder" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -97,6 +98,8 @@ func runBuild(cmd *cobra.Command, args []string) error { if len(functionName) == 0 { return fmt.Errorf("please provide the deployed --name of your function") } + analytics.Event("build", language, analyticsCh) + builder.BuildImage(image, handler, functionName, language, nocache, squash, shrinkwrap) } @@ -118,6 +121,8 @@ func build(services *stack.Services, queueDepth int, shrinkwrap bool) { fmt.Println("Please provide a valid --lang or 'Dockerfile' for your function.") } else { + analytics.Event("build", function.Language, analyticsCh) + builder.BuildImage(function.Image, function.Handler, function.Name, function.Language, nocache, squash, shrinkwrap) } } diff --git a/commands/deploy.go b/commands/deploy.go index c4952e775..a51c5d046 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -11,6 +11,7 @@ import ( "gopkg.in/yaml.v2" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/proxy" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -171,6 +172,8 @@ func runDeploy(cmd *cobra.Command, args []string) error { } } + analytics.Event("deploy", function.Language, analyticsCh) + functionResourceRequest1 := proxy.FunctionResourceRequest{ Limits: function.Limits, Requests: function.Requests, @@ -195,6 +198,8 @@ func runDeploy(cmd *cobra.Command, args []string) error { if labelErr != nil { return fmt.Errorf("error parsing labels: %v", labelErr) } + analytics.Event("deploy", language, analyticsCh) + functionResourceRequest1 := proxy.FunctionResourceRequest{} proxy.DeployFunction(fprocess, gateway, functionName, image, language, replace, envvars, network, constraints, update, secrets, labelMap, functionResourceRequest1) } diff --git a/commands/faas.go b/commands/faas.go index d02900a03..c756a3a46 100644 --- a/commands/faas.go +++ b/commands/faas.go @@ -7,21 +7,27 @@ import ( "fmt" "os" "strings" + "time" + "github.com/openfaas/faas-cli/analytics" "github.com/spf13/cobra" ) const ( - defaultGateway = "http://localhost:8080" - defaultNetwork = "func_functions" - defaultYAML = "stack.yml" + analyticsTimeout = time.Millisecond * 300 + defaultGateway = "http://localhost:8080" + defaultNetwork = "func_functions" + defaultYAML = "stack.yml" ) +var analyticsCh chan int + // Flags that are to be added to all commands. var ( - yamlFile string - regex string - filter string + yamlFile string + regex string + filter string + disableAnalytics bool ) // Flags that are to be added to subset of commands. @@ -47,6 +53,8 @@ func init() { // Set Bash completion options validYAMLFilenames := []string{"yaml", "yml"} _ = faasCmd.PersistentFlags().SetAnnotation("yaml", cobra.BashCompFilenameExt, validYAMLFilenames) + + analyticsCh = make(chan int) } // Execute TODO @@ -61,6 +69,18 @@ func Execute(customArgs []string) { fmt.Println(strings.ToUpper(e[:1]) + e[1:]) os.Exit(1) } + + if analytics.Disabled() { + return + } + + // Block on the submission of an analytics event or timeout expiration, this + // is to allow sufficient time for the event submission goroutine to complete + // when the Gateway responds extremely quickly. + select { + case <-analyticsCh: + case <-time.After(analyticsTimeout): + } } func checkAndSetDefaultYaml() { @@ -77,6 +97,9 @@ var faasCmd = &cobra.Command{ Short: "Manage your OpenFaaS functions from the command line", Long: ` Manage your OpenFaaS functions from the command line`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("root", "", analyticsCh) + }, Run: runFaas, } diff --git a/commands/invoke.go b/commands/invoke.go index 6ba4fe7fd..fe90a8ee3 100644 --- a/commands/invoke.go +++ b/commands/invoke.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/proxy" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -36,6 +37,9 @@ var invokeCmd = &cobra.Command{ Example: ` faas-cli invoke echo --gateway https://domain:port faas-cli invoke echo --gateway https://domain:port --content-type application/json faas-cli invoke env --query repo=faas-cli --query org=openfaas`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("invoke", "", analyticsCh) + }, RunE: runInvoke, } diff --git a/commands/list.go b/commands/list.go index 9fb2e8dac..7d5635140 100644 --- a/commands/list.go +++ b/commands/list.go @@ -6,6 +6,7 @@ package commands import ( "fmt" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/proxy" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -31,6 +32,9 @@ var listCmd = &cobra.Command{ Long: `Lists OpenFaaS functions either on a local or remote gateway`, Example: ` faas-cli list faas-cli list --gateway https://localhost:8080 --verbose`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("list", "", analyticsCh) + }, RunE: runList, } diff --git a/commands/login.go b/commands/login.go index d6fda90d7..ff06af54e 100644 --- a/commands/login.go +++ b/commands/login.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/config" "github.com/spf13/cobra" ) @@ -37,6 +38,9 @@ var loginCmd = &cobra.Command{ Long: "Log in to OpenFaaS gateway.\nIf no gateway is specified, the default local one will be used.", Example: ` faas-cli login -u user -p password --gateway http://localhost:8080 cat ~/faas_pass.txt | faas-cli login -u user --password-stdin --gateway https://openfaas.mydomain.com`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("login", "", analyticsCh) + }, RunE: runLogin, } diff --git a/commands/logout.go b/commands/logout.go index 7b5f5d2f5..8ad5a5407 100644 --- a/commands/logout.go +++ b/commands/logout.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/config" "github.com/spf13/cobra" ) @@ -22,7 +23,10 @@ var logoutCmd = &cobra.Command{ Short: "Log out from OpenFaaS gateway", Long: "Log out from OpenFaaS gateway.\nIf no gateway is specified, the default local one will be used.", Example: ` faas-cli logout --gateway https://openfaas.mydomain.com`, - RunE: runLogout, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("logout", "", analyticsCh) + }, + RunE: runLogout, } func runLogout(cmd *cobra.Command, args []string) error { diff --git a/commands/new_function.go b/commands/new_function.go index 8ad71c2ea..551d41eaf 100644 --- a/commands/new_function.go +++ b/commands/new_function.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/morikuni/aec" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/builder" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -81,6 +82,8 @@ the "Dockerfile" lang type in your YAML file. return fmt.Errorf("you must supply a function language with the --lang flag") } + analytics.Event("new", lang, analyticsCh) + PullTemplates("") if stack.IsValidTemplate(lang) == false { diff --git a/commands/push.go b/commands/push.go index 89f2cb93d..319c216c0 100644 --- a/commands/push.go +++ b/commands/push.go @@ -7,6 +7,7 @@ import ( "fmt" "sync" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/builder" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -32,6 +33,9 @@ These container images must already be present in your local image cache.`, faas-cli push -f ./samples.yml --parallel 4 faas-cli push -f ./samples.yml --filter "*gif*" faas-cli push -f ./samples.yml --regex "fn[0-9]_.*"`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("push", "", analyticsCh) + }, RunE: runPush, } diff --git a/commands/remove.go b/commands/remove.go index da8c7254b..a731ac0e0 100644 --- a/commands/remove.go +++ b/commands/remove.go @@ -6,6 +6,7 @@ package commands import ( "fmt" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/proxy" "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" @@ -33,6 +34,9 @@ explicitly specifying a function name.`, faas-cli remove -f ./samples.yml --regex "fn[0-9]_.*" faas-cli remove url-ping faas-cli remove img2ansi --gateway==http://remote-site.com:8080`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("remove", "", analyticsCh) + }, RunE: runDelete, } diff --git a/commands/template_pull.go b/commands/template_pull.go index 8e317ce2f..d77006138 100644 --- a/commands/template_pull.go +++ b/commands/template_pull.go @@ -7,6 +7,7 @@ import ( "os" "regexp" + "github.com/openfaas/faas-cli/analytics" "github.com/spf13/cobra" ) @@ -56,7 +57,10 @@ Currently supported verbs: %v`, supportedVerbs) Long: `Downloads the compressed github repo specified by [URL], and extracts the 'template' directory from the root of the repo, if it exists.`, Example: "faas-cli template pull https://github.com/openfaas/faas-cli", - Run: runTemplatePull, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("template pull", "", analyticsCh) + }, + Run: runTemplatePull, } func runTemplatePull(cmd *cobra.Command, args []string) { diff --git a/commands/version.go b/commands/version.go index d70190893..6374f2df0 100644 --- a/commands/version.go +++ b/commands/version.go @@ -8,6 +8,7 @@ import ( "runtime" "github.com/morikuni/aec" + "github.com/openfaas/faas-cli/analytics" "github.com/openfaas/faas-cli/version" "github.com/spf13/cobra" ) @@ -33,6 +34,9 @@ This currently consists of the GitSHA from which the client was built. - https://github.com/openfaas/faas-cli/tree/%s`, version.GitCommit), Example: ` faas-cli version faas-cli version --short-version`, + PreRun: func(cmd *cobra.Command, args []string) { + analytics.Event("version", "", analyticsCh) + }, Run: runVersion, } diff --git a/vendor.conf b/vendor.conf index ea63443d8..99ac9ce0f 100644 --- a/vendor.conf +++ b/vendor.conf @@ -1,8 +1,10 @@ -github.com/openfaas/faas 9a70a1b9915d7420e71dda445349d68f3f3928d8 +github.com/openfaas/faas be4eea92a1e2c40d19dcb04b6b9261101ac9587a gopkg.in/yaml.v2 cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b github.com/spf13/cobra 2df9a531813370438a4d79bfc33e21f58063ed87 github.com/spf13/pflag e57e3eeb33f795204c1ca35f56c44f83227c6e6 github.com/ryanuber/go-glob 256dc444b735e061061cf46c809487313d5b0065 github.com/morikuni/aec 39771216ff4c63d11f5e604076f9c45e8be1067b github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +github.com/satori/go.uuid 5bf94b69c6b68ee1b541973bb8e1144db23a194b github.com/mitchellh/go-homedir b8bc1bf767474819792c23f32d8286a45736f1c6 +github.com/google/go-querystring 53e6ce116135b80d037921a7fdd5138cf32d7a8a \ No newline at end of file diff --git a/vendor/github.com/google/go-querystring/LICENSE b/vendor/github.com/google/go-querystring/LICENSE new file mode 100644 index 000000000..ae121a1e4 --- /dev/null +++ b/vendor/github.com/google/go-querystring/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Google. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/go-querystring/README.md b/vendor/github.com/google/go-querystring/README.md new file mode 100644 index 000000000..03e937034 --- /dev/null +++ b/vendor/github.com/google/go-querystring/README.md @@ -0,0 +1,39 @@ +# go-querystring # + +go-querystring is Go library for encoding structs into URL query parameters. + + +**Documentation:** +**Build Status:** [![Build Status](https://drone.io/github.com/google/go-querystring/status.png)](https://drone.io/github.com/google/go-querystring/latest) + +## Usage ## + +```go +import "github.com/google/go-querystring/query" +``` + +go-querystring is designed to assist in scenarios where you want to construct a +URL using a struct that represents the URL query parameters. You might do this +to enforce the type safety of your parameters, for example, as is done in the +[go-github][] library. + +The query package exports a single `Values()` function. A simple example: + +```go +type Options struct { + Query string `url:"q"` + ShowAll bool `url:"all"` + Page int `url:"page"` +} + +opt := Options{ "foo", true, 2 } +v, _ := query.Values(opt) +fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" +``` + +[go-github]: https://github.com/google/go-github/commit/994f6f8405f052a117d2d0b500054341048fbb08 + +## License ## + +This library is distributed under the BSD-style license found in the [LICENSE](./LICENSE) +file. diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go new file mode 100644 index 000000000..37080b19b --- /dev/null +++ b/vendor/github.com/google/go-querystring/query/encode.go @@ -0,0 +1,320 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package query implements encoding of structs into URL query parameters. +// +// As a simple example: +// +// type Options struct { +// Query string `url:"q"` +// ShowAll bool `url:"all"` +// Page int `url:"page"` +// } +// +// opt := Options{ "foo", true, 2 } +// v, _ := query.Values(opt) +// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" +// +// The exact mapping between Go values and url.Values is described in the +// documentation for the Values() function. +package query + +import ( + "bytes" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +var timeType = reflect.TypeOf(time.Time{}) + +var encoderType = reflect.TypeOf(new(Encoder)).Elem() + +// Encoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type Encoder interface { + EncodeValues(key string, v *url.Values) error +} + +// Values returns the url.Values encoding of v. +// +// Values expects to be passed a struct, and traverses it recursively using the +// following encoding rules. +// +// Each exported struct field is encoded as a URL parameter unless +// +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option +// +// The empty values are false, 0, any nil pointer or interface value, any array +// slice, map, or string of length zero, and any time.Time that returns true +// for IsZero(). +// +// The URL parameter name defaults to the struct field name but can be +// specified in the struct field's tag value. The "url" key in the struct +// field's tag value is the key name, followed by an optional comma and +// options. For example: +// +// // Field is ignored by this package. +// Field int `url:"-"` +// +// // Field appears as URL parameter "myName". +// Field int `url:"myName"` +// +// // Field appears as URL parameter "myName" and the field is omitted if +// // its value is empty +// Field int `url:"myName,omitempty"` +// +// // Field appears as URL parameter "Field" (the default), but the field +// // is skipped if empty. Note the leading comma. +// Field int `url:",omitempty"` +// +// For encoding individual field values, the following type-dependent rules +// apply: +// +// Boolean values default to encoding as the strings "true" or "false". +// Including the "int" option signals that the field should be encoded as the +// strings "1" or "0". +// +// time.Time values default to encoding as RFC3339 timestamps. Including the +// "unix" option signals that the field should be encoded as a Unix time (see +// time.Unix()) +// +// Slice and Array values default to encoding as multiple URL values of the +// same name. Including the "comma" option signals that the field should be +// encoded as a single comma-delimited value. Including the "space" option +// similarly encodes the value as a single space-delimited string. Including +// the "semicolon" option will encode the value as a semicolon-delimited string. +// Including the "brackets" option signals that the multiple URL values should +// have "[]" appended to the value name. "numbered" will append a number to +// the end of each incidence of the value name, example: +// name0=value0&name1=value1, etc. +// +// Anonymous struct fields are usually encoded as if their inner exported +// fields were fields in the outer struct, subject to the standard Go +// visibility rules. An anonymous struct field with a name given in its URL +// tag is treated as having that name, rather than being anonymous. +// +// Non-nil pointer values are encoded as the value pointed to. +// +// Nested structs are encoded including parent fields in value names for +// scoping. e.g: +// +// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO" +// +// All other values are encoded using their default string representation. +// +// Multiple fields that encode to the same URL parameter name will be included +// as multiple URL values of the same name. +func Values(v interface{}) (url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return values, nil + } + val = val.Elem() + } + + if v == nil { + return values, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + var embedded []reflect.Value + + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if name == "" { + if sf.Anonymous && sv.Kind() == reflect.Struct { + // save embedded struct for later processing + embedded = append(embedded, sv) + continue + } + + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(encoderType) { + if !reflect.Indirect(sv).IsValid() { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(Encoder) + if err := m.EncodeValues(name, &values); err != nil { + return err + } + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + var del byte + if opts.Contains("comma") { + del = ',' + } else if opts.Contains("space") { + del = ' ' + } else if opts.Contains("semicolon") { + del = ';' + } else if opts.Contains("brackets") { + name = name + "[]" + } + + if del != 0 { + s := new(bytes.Buffer) + first := true + for i := 0; i < sv.Len(); i++ { + if first { + first = false + } else { + s.WriteByte(del) + } + s.WriteString(valueString(sv.Index(i), opts)) + } + values.Add(name, s.String()) + } else { + for i := 0; i < sv.Len(); i++ { + k := name + if opts.Contains("numbered") { + k = fmt.Sprintf("%s%d", name, i) + } + values.Add(k, valueString(sv.Index(i), opts)) + } + } + continue + } + + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == timeType { + values.Add(name, valueString(sv, opts)) + continue + } + + if sv.Kind() == reflect.Struct { + reflectValue(values, sv, name) + continue + } + + values.Add(name, valueString(sv, opts)) + } + + for _, f := range embedded { + if err := reflectValue(values, f, scope); err != nil { + return err + } + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Kind() == reflect.Bool && opts.Contains("int") { + if v.Bool() { + return "1" + } + return "0" + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if opts.Contains("unix") { + return strconv.FormatInt(t.Unix(), 10) + } + return t.Format(time.RFC3339) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + + if v.Type() == timeType { + return v.Interface().(time.Time).IsZero() + } + + return false +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/vendor/github.com/satori/go.uuid/LICENSE b/vendor/github.com/satori/go.uuid/LICENSE new file mode 100644 index 000000000..488357b8a --- /dev/null +++ b/vendor/github.com/satori/go.uuid/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2013-2016 by Maxim Bublis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/satori/go.uuid/README.md b/vendor/github.com/satori/go.uuid/README.md new file mode 100644 index 000000000..b6aad1c81 --- /dev/null +++ b/vendor/github.com/satori/go.uuid/README.md @@ -0,0 +1,65 @@ +# UUID package for Go language + +[![Build Status](https://travis-ci.org/satori/go.uuid.png?branch=master)](https://travis-ci.org/satori/go.uuid) +[![Coverage Status](https://coveralls.io/repos/github/satori/go.uuid/badge.svg?branch=master)](https://coveralls.io/github/satori/go.uuid) +[![GoDoc](http://godoc.org/github.com/satori/go.uuid?status.png)](http://godoc.org/github.com/satori/go.uuid) + +This package provides pure Go implementation of Universally Unique Identifier (UUID). Supported both creation and parsing of UUIDs. + +With 100% test coverage and benchmarks out of box. + +Supported versions: +* Version 1, based on timestamp and MAC address (RFC 4122) +* Version 2, based on timestamp, MAC address and POSIX UID/GID (DCE 1.1) +* Version 3, based on MD5 hashing (RFC 4122) +* Version 4, based on random numbers (RFC 4122) +* Version 5, based on SHA-1 hashing (RFC 4122) + +## Installation + +Use the `go` command: + + $ go get github.com/satori/go.uuid + +## Requirements + +UUID package requires Go >= 1.2. + +## Example + +```go +package main + +import ( + "fmt" + "github.com/satori/go.uuid" +) + +func main() { + // Creating UUID Version 4 + u1 := uuid.NewV4() + fmt.Printf("UUIDv4: %s\n", u1) + + // Parsing UUID from string input + u2, err := uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + if err != nil { + fmt.Printf("Something gone wrong: %s", err) + } + fmt.Printf("Successfully parsed: %s", u2) +} +``` + +## Documentation + +[Documentation](http://godoc.org/github.com/satori/go.uuid) is hosted at GoDoc project. + +## Links +* [RFC 4122](http://tools.ietf.org/html/rfc4122) +* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01) + +## Copyright + +Copyright (C) 2013-2016 by Maxim Bublis . + +UUID package released under MIT License. +See [LICENSE](https://github.com/satori/go.uuid/blob/master/LICENSE) for details. diff --git a/vendor/github.com/satori/go.uuid/uuid.go b/vendor/github.com/satori/go.uuid/uuid.go new file mode 100644 index 000000000..295f3fc2c --- /dev/null +++ b/vendor/github.com/satori/go.uuid/uuid.go @@ -0,0 +1,481 @@ +// Copyright (C) 2013-2015 by Maxim Bublis +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Package uuid provides implementation of Universally Unique Identifier (UUID). +// Supported versions are 1, 3, 4 and 5 (as specified in RFC 4122) and +// version 2 (as specified in DCE 1.1). +package uuid + +import ( + "bytes" + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "database/sql/driver" + "encoding/binary" + "encoding/hex" + "fmt" + "hash" + "net" + "os" + "sync" + "time" +) + +// UUID layout variants. +const ( + VariantNCS = iota + VariantRFC4122 + VariantMicrosoft + VariantFuture +) + +// UUID DCE domains. +const ( + DomainPerson = iota + DomainGroup + DomainOrg +) + +// Difference in 100-nanosecond intervals between +// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970). +const epochStart = 122192928000000000 + +// Used in string method conversion +const dash byte = '-' + +// UUID v1/v2 storage. +var ( + storageMutex sync.Mutex + storageOnce sync.Once + epochFunc = unixTimeFunc + clockSequence uint16 + lastTime uint64 + hardwareAddr [6]byte + posixUID = uint32(os.Getuid()) + posixGID = uint32(os.Getgid()) +) + +// String parse helpers. +var ( + urnPrefix = []byte("urn:uuid:") + byteGroups = []int{8, 4, 4, 4, 12} +) + +func initClockSequence() { + buf := make([]byte, 2) + safeRandom(buf) + clockSequence = binary.BigEndian.Uint16(buf) +} + +func initHardwareAddr() { + interfaces, err := net.Interfaces() + if err == nil { + for _, iface := range interfaces { + if len(iface.HardwareAddr) >= 6 { + copy(hardwareAddr[:], iface.HardwareAddr) + return + } + } + } + + // Initialize hardwareAddr randomly in case + // of real network interfaces absence + safeRandom(hardwareAddr[:]) + + // Set multicast bit as recommended in RFC 4122 + hardwareAddr[0] |= 0x01 +} + +func initStorage() { + initClockSequence() + initHardwareAddr() +} + +func safeRandom(dest []byte) { + if _, err := rand.Read(dest); err != nil { + panic(err) + } +} + +// Returns difference in 100-nanosecond intervals between +// UUID epoch (October 15, 1582) and current time. +// This is default epoch calculation function. +func unixTimeFunc() uint64 { + return epochStart + uint64(time.Now().UnixNano()/100) +} + +// UUID representation compliant with specification +// described in RFC 4122. +type UUID [16]byte + +// NullUUID can be used with the standard sql package to represent a +// UUID value that can be NULL in the database +type NullUUID struct { + UUID UUID + Valid bool +} + +// The nil UUID is special form of UUID that is specified to have all +// 128 bits set to zero. +var Nil = UUID{} + +// Predefined namespace UUIDs. +var ( + NamespaceDNS, _ = FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + NamespaceURL, _ = FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + NamespaceOID, _ = FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8") + NamespaceX500, _ = FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8") +) + +// And returns result of binary AND of two UUIDs. +func And(u1 UUID, u2 UUID) UUID { + u := UUID{} + for i := 0; i < 16; i++ { + u[i] = u1[i] & u2[i] + } + return u +} + +// Or returns result of binary OR of two UUIDs. +func Or(u1 UUID, u2 UUID) UUID { + u := UUID{} + for i := 0; i < 16; i++ { + u[i] = u1[i] | u2[i] + } + return u +} + +// Equal returns true if u1 and u2 equals, otherwise returns false. +func Equal(u1 UUID, u2 UUID) bool { + return bytes.Equal(u1[:], u2[:]) +} + +// Version returns algorithm version used to generate UUID. +func (u UUID) Version() uint { + return uint(u[6] >> 4) +} + +// Variant returns UUID layout variant. +func (u UUID) Variant() uint { + switch { + case (u[8] & 0x80) == 0x00: + return VariantNCS + case (u[8]&0xc0)|0x80 == 0x80: + return VariantRFC4122 + case (u[8]&0xe0)|0xc0 == 0xc0: + return VariantMicrosoft + } + return VariantFuture +} + +// Bytes returns bytes slice representation of UUID. +func (u UUID) Bytes() []byte { + return u[:] +} + +// Returns canonical string representation of UUID: +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. +func (u UUID) String() string { + buf := make([]byte, 36) + + hex.Encode(buf[0:8], u[0:4]) + buf[8] = dash + hex.Encode(buf[9:13], u[4:6]) + buf[13] = dash + hex.Encode(buf[14:18], u[6:8]) + buf[18] = dash + hex.Encode(buf[19:23], u[8:10]) + buf[23] = dash + hex.Encode(buf[24:], u[10:]) + + return string(buf) +} + +// SetVersion sets version bits. +func (u *UUID) SetVersion(v byte) { + u[6] = (u[6] & 0x0f) | (v << 4) +} + +// SetVariant sets variant bits as described in RFC 4122. +func (u *UUID) SetVariant() { + u[8] = (u[8] & 0xbf) | 0x80 +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The encoding is the same as returned by String. +func (u UUID) MarshalText() (text []byte, err error) { + text = []byte(u.String()) + return +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// Following formats are supported: +// "6ba7b810-9dad-11d1-80b4-00c04fd430c8", +// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}", +// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" +func (u *UUID) UnmarshalText(text []byte) (err error) { + if len(text) < 32 { + err = fmt.Errorf("uuid: UUID string too short: %s", text) + return + } + + t := text[:] + braced := false + + if bytes.Equal(t[:9], urnPrefix) { + t = t[9:] + } else if t[0] == '{' { + braced = true + t = t[1:] + } + + b := u[:] + + for i, byteGroup := range byteGroups { + if i > 0 { + if t[0] != '-' { + err = fmt.Errorf("uuid: invalid string format") + return + } + t = t[1:] + } + + if len(t) < byteGroup { + err = fmt.Errorf("uuid: UUID string too short: %s", text) + return + } + + if i == 4 && len(t) > byteGroup && + ((braced && t[byteGroup] != '}') || len(t[byteGroup:]) > 1 || !braced) { + err = fmt.Errorf("uuid: UUID string too long: %s", text) + return + } + + _, err = hex.Decode(b[:byteGroup/2], t[:byteGroup]) + if err != nil { + return + } + + t = t[byteGroup:] + b = b[byteGroup/2:] + } + + return +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (u UUID) MarshalBinary() (data []byte, err error) { + data = u.Bytes() + return +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +// It will return error if the slice isn't 16 bytes long. +func (u *UUID) UnmarshalBinary(data []byte) (err error) { + if len(data) != 16 { + err = fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data)) + return + } + copy(u[:], data) + + return +} + +// Value implements the driver.Valuer interface. +func (u UUID) Value() (driver.Value, error) { + return u.String(), nil +} + +// Scan implements the sql.Scanner interface. +// A 16-byte slice is handled by UnmarshalBinary, while +// a longer byte slice or a string is handled by UnmarshalText. +func (u *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + if len(src) == 16 { + return u.UnmarshalBinary(src) + } + return u.UnmarshalText(src) + + case string: + return u.UnmarshalText([]byte(src)) + } + + return fmt.Errorf("uuid: cannot convert %T to UUID", src) +} + +// Value implements the driver.Valuer interface. +func (u NullUUID) Value() (driver.Value, error) { + if !u.Valid { + return nil, nil + } + // Delegate to UUID Value function + return u.UUID.Value() +} + +// Scan implements the sql.Scanner interface. +func (u *NullUUID) Scan(src interface{}) error { + if src == nil { + u.UUID, u.Valid = Nil, false + return nil + } + + // Delegate to UUID Scan function + u.Valid = true + return u.UUID.Scan(src) +} + +// FromBytes returns UUID converted from raw byte slice input. +// It will return error if the slice isn't 16 bytes long. +func FromBytes(input []byte) (u UUID, err error) { + err = u.UnmarshalBinary(input) + return +} + +// FromBytesOrNil returns UUID converted from raw byte slice input. +// Same behavior as FromBytes, but returns a Nil UUID on error. +func FromBytesOrNil(input []byte) UUID { + uuid, err := FromBytes(input) + if err != nil { + return Nil + } + return uuid +} + +// FromString returns UUID parsed from string input. +// Input is expected in a form accepted by UnmarshalText. +func FromString(input string) (u UUID, err error) { + err = u.UnmarshalText([]byte(input)) + return +} + +// FromStringOrNil returns UUID parsed from string input. +// Same behavior as FromString, but returns a Nil UUID on error. +func FromStringOrNil(input string) UUID { + uuid, err := FromString(input) + if err != nil { + return Nil + } + return uuid +} + +// Returns UUID v1/v2 storage state. +// Returns epoch timestamp, clock sequence, and hardware address. +func getStorage() (uint64, uint16, []byte) { + storageOnce.Do(initStorage) + + storageMutex.Lock() + defer storageMutex.Unlock() + + timeNow := epochFunc() + // Clock changed backwards since last UUID generation. + // Should increase clock sequence. + if timeNow <= lastTime { + clockSequence++ + } + lastTime = timeNow + + return timeNow, clockSequence, hardwareAddr[:] +} + +// NewV1 returns UUID based on current timestamp and MAC address. +func NewV1() UUID { + u := UUID{} + + timeNow, clockSeq, hardwareAddr := getStorage() + + binary.BigEndian.PutUint32(u[0:], uint32(timeNow)) + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) + binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) + binary.BigEndian.PutUint16(u[8:], clockSeq) + + copy(u[10:], hardwareAddr) + + u.SetVersion(1) + u.SetVariant() + + return u +} + +// NewV2 returns DCE Security UUID based on POSIX UID/GID. +func NewV2(domain byte) UUID { + u := UUID{} + + timeNow, clockSeq, hardwareAddr := getStorage() + + switch domain { + case DomainPerson: + binary.BigEndian.PutUint32(u[0:], posixUID) + case DomainGroup: + binary.BigEndian.PutUint32(u[0:], posixGID) + } + + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) + binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) + binary.BigEndian.PutUint16(u[8:], clockSeq) + u[9] = domain + + copy(u[10:], hardwareAddr) + + u.SetVersion(2) + u.SetVariant() + + return u +} + +// NewV3 returns UUID based on MD5 hash of namespace UUID and name. +func NewV3(ns UUID, name string) UUID { + u := newFromHash(md5.New(), ns, name) + u.SetVersion(3) + u.SetVariant() + + return u +} + +// NewV4 returns random generated UUID. +func NewV4() UUID { + u := UUID{} + safeRandom(u[:]) + u.SetVersion(4) + u.SetVariant() + + return u +} + +// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name. +func NewV5(ns UUID, name string) UUID { + u := newFromHash(sha1.New(), ns, name) + u.SetVersion(5) + u.SetVariant() + + return u +} + +// Returns UUID based on hashing of namespace UUID and name. +func newFromHash(h hash.Hash, ns UUID, name string) UUID { + u := UUID{} + h.Write(ns[:]) + h.Write([]byte(name)) + copy(u[:], h.Sum(nil)) + + return u +}