diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd019c4df..3a5b6c313 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,7 @@ jobs: # either 'goreleaser' (default) or 'goreleaser-pro': distribution: goreleaser version: latest + workdir: ./build/.goreleaser.yaml args: release --clean env: GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index de6570dd9..f6cbe10c1 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ all: tidy gen add-copyright format lint cover build # Build set ROOT_PACKAGE=github.com/OpenIMSDK/Open-IM-Server -# TODO: This is version control for the future +# TODO: This is version control for the future https://github.com/OpenIMSDK/Open-IM-Server/issues/574 VERSION_PACKAGE=github.com/OpenIMSDK/Open-IM-Server/pkg/version # ============================================================================== diff --git a/.goreleaser.yaml b/build/.goreleaser.yaml similarity index 100% rename from .goreleaser.yaml rename to build/.goreleaser.yaml diff --git a/go.work b/go.work index 09e86f032..88aeaf301 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,8 @@ go 1.20 use ( - . + . + ./tools/changelog ./tools/infra ./tools/ncpu ) diff --git a/scripts/build_all_service.sh b/scripts/build_all_service.sh index 24bb03665..eb40e5f78 100755 --- a/scripts/build_all_service.sh +++ b/scripts/build_all_service.sh @@ -19,21 +19,8 @@ SCRIPTS_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) OPENIM_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. #Include shell font styles and some basic information -source $SCRIPTS_ROOT/style_info.sh source $SCRIPTS_ROOT/path_info.sh -source $SCRIPTS_ROOT/function.sh - -echo -e "${YELLOW_PREFIX}=======>SCRIPTS_ROOT=$SCRIPTS_ROOT${COLOR_SUFFIX}" -echo -e "${YELLOW_PREFIX}=======>OPENIM_ROOT=$OPENIM_ROOT${COLOR_SUFFIX}" -echo -e "${YELLOW_PREFIX}=======>pwd=$PWD${COLOR_SUFFIX}" - -echo -e "" - -echo -e "${BACKGROUND_BLUE}===============> Building all using make build binary files ${COLOR_SUFFIX}" - -echo -e "" -echo -e "${BOLD_PREFIX}____________________________________________________________ ${COLOR_SUFFIX}" - +source $SCRIPTS_ROOT/lib/init.sh bin_dir="$BIN_DIR" logs_dir="$OPENIM_ROOT/logs" @@ -68,43 +55,7 @@ if [ $? -ne 0 ]; then exit 1 fi -# Get the current operating system and architecture -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m) - -# Select the repository home directory based on the operating system and architecture -if [[ "$OS" == "darwin" ]]; then - if [[ "$ARCH" == "x86_64" ]]; then - REPO_DIR="darwin/amd64" - else - REPO_DIR="darwin/386" - fi -elif [[ "$OS" == "linux" ]]; then - if [[ "$ARCH" == "x86_64" ]]; then - REPO_DIR="linux/amd64" - elif [[ "$ARCH" == "arm64" ]]; then - REPO_DIR="linux/arm64" - elif [[ "$ARCH" == "mips64" ]]; then - REPO_DIR="linux/mips64" - elif [[ "$ARCH" == "mips64le" ]]; then - REPO_DIR="linux/mips64le" - elif [[ "$ARCH" == "ppc64le" ]]; then - REPO_DIR="linux/ppc64le" - elif [[ "$ARCH" == "s390x" ]]; then - REPO_DIR="linux/s390x" - else - REPO_DIR="linux/386" - fi -elif [[ "$OS" == "windows" ]]; then - if [[ "$ARCH" == "x86_64" ]]; then - REPO_DIR="windows/amd64" - else - REPO_DIR="windows/386" - fi -else - echo -e "${RED_PREFIX}Unsupported OS: $OS${COLOR_SUFFIX}" - exit 1 -fi +gen_os_arch # Determine if all scripts were successfully built BUILD_SUCCESS=true diff --git a/scripts/lib/util.sh b/scripts/lib/util.sh index f9a55d37c..7485e9e55 100755 --- a/scripts/lib/util.sh +++ b/scripts/lib/util.sh @@ -729,3 +729,43 @@ function remove_space() { value=$* # 获取传入的参数 result=$(echo $value | sed 's/ //g') # 去除空格 } + +function gen_os_arch() { + # Get the current operating system and architecture + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + # Select the repository home directory based on the operating system and architecture + if [[ "$OS" == "darwin" ]]; then + if [[ "$ARCH" == "x86_64" ]]; then + REPO_DIR="darwin/amd64" + else + REPO_DIR="darwin/386" + fi + elif [[ "$OS" == "linux" ]]; then + if [[ "$ARCH" == "x86_64" ]]; then + REPO_DIR="linux/amd64" + elif [[ "$ARCH" == "arm64" ]]; then + REPO_DIR="linux/arm64" + elif [[ "$ARCH" == "mips64" ]]; then + REPO_DIR="linux/mips64" + elif [[ "$ARCH" == "mips64le" ]]; then + REPO_DIR="linux/mips64le" + elif [[ "$ARCH" == "ppc64le" ]]; then + REPO_DIR="linux/ppc64le" + elif [[ "$ARCH" == "s390x" ]]; then + REPO_DIR="linux/s390x" + else + REPO_DIR="linux/386" + fi + elif [[ "$OS" == "windows" ]]; then + if [[ "$ARCH" == "x86_64" ]]; then + REPO_DIR="windows/amd64" + else + REPO_DIR="windows/386" + fi + else + echo -e "${RED_PREFIX}Unsupported OS: $OS${COLOR_SUFFIX}" + exit 1 + fi +} \ No newline at end of file diff --git a/scripts/make-rules/common.mk b/scripts/make-rules/common.mk index 0da5af561..051f1697d 100644 --- a/scripts/make-rules/common.mk +++ b/scripts/make-rules/common.mk @@ -126,7 +126,7 @@ FIND := find . ! -path './utils/*' ! -path './vendor/*' ! -path './third_party/* XARGS := xargs -r --no-run-if-empty # Linux command settings-CODE DIRS Copyright -CODE_DIRS := $(ROOT_DIR)/pkg $(ROOT_DIR)/cmd $(ROOT_DIR)/config $(ROOT_DIR)/db $(ROOT_DIR)/.docker-compose_cfg $(ROOT_DIR)/internal $(ROOT_DIR)/scripts $(ROOT_DIR)/test $(ROOT_DIR)/.github $(ROOT_DIR)/build $(ROOT_DIR)/build $(ROOT_DIR)/deployments +CODE_DIRS := $(ROOT_DIR)/pkg $(ROOT_DIR)/cmd $(ROOT_DIR)/config $(ROOT_DIR)/db $(ROOT_DIR)/.docker-compose_cfg $(ROOT_DIR)/internal $(ROOT_DIR)/scripts $(ROOT_DIR)/test $(ROOT_DIR)/.github $(ROOT_DIR)/build $(ROOT_DIR)/tools $(ROOT_DIR)/deployments FINDS := find $(CODE_DIRS) # Makefile settings: Select different behaviors by determining whether V option is set diff --git a/scripts/path_info.sh b/scripts/path_info.sh index d3f6f4e1c..e8d89bb88 100755 --- a/scripts/path_info.sh +++ b/scripts/path_info.sh @@ -22,7 +22,7 @@ SCRIPTS_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) OPENIM_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. #Include shell font styles and some basic information -source $SCRIPTS_ROOT/style_info.sh +source $SCRIPTS_ROOT/lib/color.sh cd $SCRIPTS_ROOT diff --git a/scripts/stop_all.sh b/scripts/stop_all.sh index 772e68d15..9006c1aaa 100755 --- a/scripts/stop_all.sh +++ b/scripts/stop_all.sh @@ -29,7 +29,7 @@ cd "$SCRIPTS_ROOT" for i in ${service_names[*]}; do #Check whether the service exists - name="ps |grep -w $i |grep -v grep" + name="ps aux |grep -w $i |grep -v grep" count="${name}| wc -l" if [ $(eval ${count}) -gt 0 ]; then pid="${name}| awk '{print \$2}'" diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..16f75d5a4 --- /dev/null +++ b/test/README.md @@ -0,0 +1,15 @@ +## Run the Tests + +To run a single test or set of tests, you'll need the [Ginkgo](https://github.com/onsi/ginkgo) tool installed on your +machine: + +```console +go install github.com/onsi/ginkgo/ginkgo@latest +``` + +```shell +ginkgo --help + --focus value + If set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed. + +``` diff --git a/test/jwt/main.go b/test/jwt/main.go new file mode 100644 index 000000000..5111139d4 --- /dev/null +++ b/test/jwt/main.go @@ -0,0 +1,38 @@ +// Copyright 2020 Lingfei Kong . All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + + "github.com/golang-jwt/jwt/v4" +) + +func main() { + rawJWT := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJpYW0uYXV0aHoubWFybW90ZWR1LmNvbSIsImV4cCI6MTYwNDEyODQwMywiaWF0IjoxNjA0MTI4NDAyLCJpc3MiOiJpYW1jdGwiLCJraWQiOiJpZDEifQ.Itr5u4C-nTeA01qbjjl7RzuPD-aSQazsJZY_Z25aGnI` + + // Verify the token + claims := &jwt.MapClaims{} + parsedT, err := jwt.ParseWithClaims(rawJWT, claims, func(token *jwt.Token) (interface{}, error) { + // Validate the alg is HMAC signature + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + if kid, ok := token.Header["kid"].(string); ok { + fmt.Println("kid", kid) + } + + return []byte("key1"), nil + }) + + if err != nil || !parsedT.Valid { + fmt.Println("token valid failed", err) + + return + } + + fmt.Println("ok") +} diff --git a/test/testdata/README.md b/test/testdata/README.md new file mode 100644 index 000000000..74cdb71f8 --- /dev/null +++ b/test/testdata/README.md @@ -0,0 +1,64 @@ + +# Test Data for OpenIM Server + +This directory (`testdata`) contains various JSON formatted data files that are used for testing the OpenIM Server. + +## Structure + +```bash +testdata/ +│ +├── README.md # 描述该目录下各子目录和文件的作用 +│ +├── db/ # 存储模拟的数据库数据 +│ ├── users.json # 用户的模拟数据 +│ └── messages.json # 消息的模拟数据 +│ +├── requests/ # 存储模拟的请求数据 +│ ├── login.json # 模拟登陆请求 +│ ├── register.json # 模拟注册请求 +│ └── sendMessage.json # 模拟发送消息请求 +│ +└── responses/ # 存储模拟的响应数据 + ├── login.json # 模拟登陆响应 + ├── register.json # 模拟注册响应 + └── sendMessage.json # 模拟发送消息响应 +``` + +Here is an overview of what each subdirectory or file represents: + +- `db/` - This directory contains mock data mimicking the actual database contents. + - `users.json` - Represents a list of users in the system. Each entry contains user-specific information such as user ID, username, password hash, etc. + - `messages.json` - Contains a list of messages exchanged between users. Each message entry includes the sender's and receiver's user IDs, message content, timestamp, etc. +- `requests/` - This directory contains mock requests that a client might send to the server. + - `login.json` - Represents a user login request. It includes fields such as username and password. + - `register.json` - Mimics a user registration request. Contains details such as username, password, email, etc. + - `sendMessage.json` - Simulates a message sending request from a user to another user. +- `responses/` - This directory holds the expected server responses for the respective requests. + - `login.json` - Represents a successful login response from the server. It typically includes a session token and user-specific information. + - `register.json` - Simulates a successful registration response from the server, usually containing the new user's ID, username, etc. + - `sendMessage.json` - Depicts a successful message sending response from the server, confirming the delivery of the message. + +## JSON Format + +All the data files in this directory are in JSON format. JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate. + +Here is a simple example of what a JSON file might look like: + +```bash + "users": [ + { + "id": 1, + "username": "user1", + "password": "password1" + }, + { + "id": 2, + "username": "user2", + "password": "password2" + } + ] + +``` + +In this example, "users" is an array of user objects. Each user object has an "id", "username", and "password". diff --git a/test/testdata/db/messages.json b/test/testdata/db/messages.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/db/users.json b/test/testdata/db/users.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/requests/login.json b/test/testdata/requests/login.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/requests/register.json b/test/testdata/requests/register.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/requests/sendMessage.json b/test/testdata/requests/sendMessage.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/responses/login.json b/test/testdata/responses/login.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/responses/register.json b/test/testdata/responses/register.json new file mode 100644 index 000000000..e69de29bb diff --git a/test/testdata/responses/sendMessage.json b/test/testdata/responses/sendMessage.json new file mode 100644 index 000000000..e69de29bb diff --git a/tools/changelog/changelog.go b/tools/changelog/changelog.go new file mode 100644 index 000000000..7428db5fa --- /dev/null +++ b/tools/changelog/changelog.go @@ -0,0 +1,308 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 main + +import ( + "fmt" + "log" + "os" + "os/exec" + "regexp" + "sort" + "strings" +) + +var ( + mergeRequest = regexp.MustCompile(`Merge pull request #([\d]+)`) + webconsoleBump = regexp.MustCompile(regexp.QuoteMeta("bump(github.com/openshift/origin-web-console): ") + `([\w]+)`) + upstreamKube = regexp.MustCompile(`^UPSTREAM: (\d+)+:(.+)`) + upstreamRepo = regexp.MustCompile(`^UPSTREAM: ([\w/-]+): (\d+)+:(.+)`) + prefix = regexp.MustCompile(`^[\w-]: `) + + assignments = []prefixAssignment{ + {"cluster up", "cluster"}, + {" pv ", "storage"}, + {"haproxy", "router"}, + {"router", "router"}, + {"route", "route"}, + {"authoriz", "auth"}, + {"rbac", "auth"}, + {"authent", "auth"}, + {"reconcil", "auth"}, + {"auth", "auth"}, + {"role", "auth"}, + {" dc ", "deploy"}, + {"deployment", "deploy"}, + {"rolling", "deploy"}, + {"security context constr", "security"}, + {"scc", "security"}, + {"pipeline", "build"}, + {"build", "build"}, + {"registry", "registry"}, + {"registries", "image"}, + {"image", "image"}, + {" arp ", "network"}, + {" cni ", "network"}, + {"egress", "network"}, + {"network", "network"}, + {"oc ", "cli"}, + {"template", "template"}, + {"etcd", "server"}, + {"pod", "node"}, + {"hack/", "hack"}, + {"e2e", "test"}, + {"integration", "test"}, + {"cluster", "cluster"}, + {"master", "server"}, + {"packages", "hack"}, + {"api", "server"}, + } +) + +type prefixAssignment struct { + term string + prefix string +} + +type commit struct { + short string + parents []string + message string +} + +func contains(arr []string, value string) bool { + for _, s := range arr { + if s == value { + return true + } + } + return false +} + +func main() { + log.SetFlags(0) + if len(os.Args) != 3 { + log.Fatalf("Must specify two arguments, FROM and TO") + } + from := os.Args[1] + to := os.Args[2] + + out, err := exec.Command("git", "log", "--topo-order", "--pretty=tformat:%h %p|%s", "--reverse", fmt.Sprintf("%s..%s", from, to)).CombinedOutput() + if err != nil { + log.Fatal(err) + } + + hide := make(map[string]struct{}) + var apiChanges []string + var webconsole []string + var commits []commit + var upstreams []commit + var bumps []commit + for _, line := range strings.Split(string(out), "\n") { + if len(strings.TrimSpace(line)) == 0 { + continue + } + parts := strings.SplitN(line, "|", 2) + hashes := strings.Split(parts[0], " ") + c := commit{short: hashes[0], parents: hashes[1:], message: parts[1]} + + if strings.HasPrefix(c.message, "UPSTREAM: ") { + hide[c.short] = struct{}{} + upstreams = append(upstreams, c) + } + if strings.HasPrefix(c.message, "bump(") { + hide[c.short] = struct{}{} + bumps = append(bumps, c) + } + + if len(c.parents) == 1 { + commits = append(commits, c) + continue + } + + matches := mergeRequest.FindStringSubmatch(line) + if len(matches) == 0 { + // this may have been a human pressing the merge button, we'll just record this as a direct push + continue + } + + // split the accumulated commits into any that are force merges (assumed to be the initial set due + // to --topo-order) from the PR commits as soon as we see any of our merge parents. Then print + // any of the force merges + var first int + for i := range commits { + first = i + if contains(c.parents, commits[i].short) { + first++ + break + } + } + individual := commits[:first] + merged := commits[first:] + for _, commit := range individual { + if len(commit.parents) > 1 { + continue + } + if _, ok := hide[commit.short]; ok { + continue + } + fmt.Printf("force-merge: %s %s\n", commit.message, commit.short) + } + + // try to find either the PR title or the first commit title from the merge commit + out, err := exec.Command("git", "show", "--pretty=tformat:%b", c.short).CombinedOutput() + if err != nil { + log.Fatal(err) + } + var message string + para := strings.Split(string(out), "\n\n") + if len(para) > 0 && strings.HasPrefix(para[0], "Automatic merge from submit-queue") { + para = para[1:] + } + // this is no longer necessary with the submit queue in place + if len(para) > 0 && strings.HasPrefix(para[0], "Merged by ") { + para = para[1:] + } + // post submit-queue, the merge bot will add the PR title, which is usually pretty good + if len(para) > 0 { + message = strings.Split(para[0], "\n")[0] + } + if len(message) == 0 && len(merged) > 0 { + message = merged[0].message + } + if len(message) > 0 && len(merged) == 1 && message == merged[0].message { + merged = nil + } + + // try to calculate a prefix based on the diff + if len(message) > 0 && !prefix.MatchString(message) { + prefix, ok := findPrefixFor(message, merged) + if ok { + message = prefix + ": " + message + } + } + + // github merge + + // has api changes + display := fmt.Sprintf("%s [\\#%s](https://github.com/openimsdk/Open-IM-Server/pull/%s)", message, matches[1], matches[1]) + if hasFileChanges(c.short, "pkg/apistruct/") { + apiChanges = append(apiChanges, display) + } + + var filtered []commit + for _, commit := range merged { + if _, ok := hide[commit.short]; ok { + continue + } + filtered = append(filtered, commit) + } + if len(filtered) > 0 { + fmt.Printf("- %s\n", display) + for _, commit := range filtered { + fmt.Printf(" - %s (%s)\n", commit.message, commit.short) + } + } + + // stick the merge commit in at the beginning of the next list so we can anchor the previous parent + commits = []commit{c} + } + + // chunk the bumps + var lines []string + for _, commit := range bumps { + if m := webconsoleBump.FindStringSubmatch(commit.message); len(m) > 0 { + webconsole = append(webconsole, m[1]) + continue + } + lines = append(lines, commit.message) + } + lines = sortAndUniq(lines) + for _, line := range lines { + fmt.Printf("- %s\n", line) + } + + // chunk the upstreams + lines = nil + for _, commit := range upstreams { + lines = append(lines, commit.message) + } + lines = sortAndUniq(lines) + for _, line := range lines { + fmt.Printf("- %s\n", upstreamLinkify(line)) + } + + if len(webconsole) > 0 { + fmt.Printf("- web: from %s^..%s\n", webconsole[0], webconsole[len(webconsole)-1]) + } + + for _, apiChange := range apiChanges { + fmt.Printf(" - %s\n", apiChange) + } +} + +func findPrefixFor(message string, commits []commit) (string, bool) { + message = strings.ToLower(message) + for _, m := range assignments { + if strings.Contains(message, m.term) { + return m.prefix, true + } + } + for _, c := range commits { + if prefix, ok := findPrefixFor(c.message, nil); ok { + return prefix, ok + } + } + return "", false +} + +func hasFileChanges(commit string, prefixes ...string) bool { + out, err := exec.Command("git", "diff", "--name-only", fmt.Sprintf("%s^..%s", commit, commit)).CombinedOutput() + if err != nil { + log.Fatal(err) + } + for _, file := range strings.Split(string(out), "\n") { + for _, prefix := range prefixes { + if strings.HasPrefix(file, prefix) { + return true + } + } + } + return false +} + +func sortAndUniq(lines []string) []string { + sort.Strings(lines) + out := make([]string, 0, len(lines)) + last := "" + for _, s := range lines { + if last == s { + continue + } + last = s + out = append(out, s) + } + return out +} + +func upstreamLinkify(line string) string { + if m := upstreamKube.FindStringSubmatch(line); len(m) > 0 { + return fmt.Sprintf("UPSTREAM: [#%s](https://github.com/OpenIMSDK/Open-IM-Server/pull/%s):%s", m[1], m[1], m[2]) + } + if m := upstreamRepo.FindStringSubmatch(line); len(m) > 0 { + return fmt.Sprintf("UPSTREAM: [%s#%s](https://github.com/%s/pull/%s):%s", m[1], m[2], m[1], m[2], m[3]) + } + return line +} \ No newline at end of file diff --git a/tools/changelog/go.mod b/tools/changelog/go.mod new file mode 100644 index 000000000..319cdecec --- /dev/null +++ b/tools/changelog/go.mod @@ -0,0 +1,3 @@ +module github.com/OpenIMSDK/Open-IM-Server/tools/changelog + +go 1.20 diff --git a/tools/infra/main.go b/tools/infra/main.go index 55243d386..cc20a17c7 100644 --- a/tools/infra/main.go +++ b/tools/infra/main.go @@ -1,3 +1,17 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 main import ( diff --git a/tools/ncpu/main.go b/tools/ncpu/main.go index b55852554..7ca3dff5e 100644 --- a/tools/ncpu/main.go +++ b/tools/ncpu/main.go @@ -1,3 +1,17 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 main import (