mirror of
https://github.com/hanzoai/docdb.git
synced 2026-10-30 18:39:39 +00:00
347 lines
8.8 KiB
Go
347 lines
8.8 KiB
Go
// Copyright 2021 Hanzo AI Inc.
|
|
//
|
|
// 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 (
|
|
"bytes"
|
|
"context"
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kong"
|
|
|
|
"github.com/hanzoai/docdb/build/version"
|
|
"github.com/hanzoai/docdb/internal/util/ctxutil"
|
|
"github.com/hanzoai/docdb/internal/util/logging"
|
|
)
|
|
|
|
var (
|
|
//go:embed error.tmpl
|
|
errorTemplateB []byte
|
|
|
|
// Parsed error template.
|
|
errorTemplate = template.Must(template.New("error").Option("missingkey=error").Parse(string(errorTemplateB)))
|
|
)
|
|
|
|
// versionFile contains version information with leading v.
|
|
const versionFile = "build/version/version.txt"
|
|
|
|
// runCommand runs command with given arguments.
|
|
func runCommand(command string, args []string, stdout io.Writer, logger *slog.Logger) error {
|
|
bin, err := exec.LookPath(command)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command(bin, args...)
|
|
logger.Debug(fmt.Sprintf("Running %s", strings.Join(cmd.Args, " ")))
|
|
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("%s failed: %s", strings.Join(cmd.Args, " "), err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// printDiagnosticData prints diagnostic data and error template on stdout.
|
|
func printDiagnosticData(w io.Writer, setupError error, logger *slog.Logger) error {
|
|
_ = runCommand("docker", []string{"compose", "logs"}, w, logger)
|
|
|
|
_ = runCommand("docker", []string{"compose", "ps", "--all"}, w, logger)
|
|
|
|
_ = runCommand("docker", []string{"stats", "--all", "--no-stream"}, w, logger)
|
|
|
|
var buf bytes.Buffer
|
|
|
|
var gitVersion string
|
|
if err := runCommand("git", []string{"version"}, &buf, logger); err != nil {
|
|
gitVersion = err.Error()
|
|
} else {
|
|
gitVersion = buf.String()
|
|
}
|
|
|
|
buf.Reset()
|
|
|
|
var dockerVersion string
|
|
if err := runCommand("docker", []string{"version"}, &buf, logger); err != nil {
|
|
dockerVersion = err.Error()
|
|
} else {
|
|
dockerVersion = buf.String()
|
|
}
|
|
|
|
buf.Reset()
|
|
|
|
var composeVersion string
|
|
if err := runCommand("docker", []string{"compose", "version"}, &buf, logger); err != nil {
|
|
composeVersion = err.Error()
|
|
} else {
|
|
composeVersion = buf.String()
|
|
}
|
|
|
|
info := version.Get()
|
|
|
|
return errorTemplate.Execute(w, map[string]any{
|
|
"Error": setupError,
|
|
|
|
"GOOS": runtime.GOOS,
|
|
"GOARCH": runtime.GOARCH,
|
|
|
|
"Version": info.Version,
|
|
"Commit": info.Commit,
|
|
"Branch": info.Branch,
|
|
"Dirty": info.Dirty,
|
|
"Package": info.Package,
|
|
"DevBuild": info.DevBuild,
|
|
|
|
"GoVersion": runtime.Version(),
|
|
"GitVersion": strings.TrimSpace(gitVersion),
|
|
"DockerVersion": strings.TrimSpace(dockerVersion),
|
|
"ComposeVersion": strings.TrimSpace(composeVersion),
|
|
|
|
"NewIssueURL": "https://github.com/hanzoai/docdb/issues/new/choose",
|
|
})
|
|
}
|
|
|
|
// shellMkDir creates all directories from given paths.
|
|
func shellMkDir(paths ...string) error {
|
|
var errs error
|
|
|
|
for _, path := range paths {
|
|
if err := os.MkdirAll(path, 0o777); err != nil {
|
|
errs = errors.Join(errs, err)
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// shellRmDir removes all directories from given paths.
|
|
func shellRmDir(paths ...string) error {
|
|
var errs error
|
|
|
|
for _, path := range paths {
|
|
if err := os.RemoveAll(path); err != nil {
|
|
errs = errors.Join(errs, err)
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// shellRead will show the content of a file.
|
|
func shellRead(w io.Writer, paths ...string) error {
|
|
for _, path := range paths {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprint(w, string(b))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// packageVersion will print out DocDB package version (omitting leading v).
|
|
func packageVersion(w io.Writer, file string) error {
|
|
b, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
v := string(b)
|
|
v = strings.TrimPrefix(v, "v")
|
|
|
|
_, err = fmt.Fprint(w, v)
|
|
|
|
return err
|
|
}
|
|
|
|
// TestsRunParams represents `envtool tests run` parameters.
|
|
//
|
|
//nolint:vet // for readability
|
|
type TestsRunParams struct {
|
|
ShardIndex uint `help:"Shard index, starting from 1."`
|
|
ShardTotal uint `help:"Total number of shards."`
|
|
Run string `help:"Run only tests matching the regexp."`
|
|
Skip string `help:"Skip tests matching the regexp."`
|
|
RawPrefix string `help:"Prefix for raw output files."`
|
|
|
|
Args []string `arg:"" help:"Other arguments and flags for 'go test'." passthrough:""`
|
|
}
|
|
|
|
// cli struct represents all command-line commands, fields and flags.
|
|
// It's used for parsing the user input.
|
|
//
|
|
//nolint:vet // for readability
|
|
var cli struct {
|
|
Debug bool `help:"Enable debug mode."`
|
|
|
|
Setup struct{} `cmd:"" help:"Setup development environment."`
|
|
|
|
PackageVersion struct{} `cmd:"" help:"Print package version."`
|
|
|
|
Shell struct {
|
|
Mkdir struct {
|
|
Paths []string `name:"path" arg:"" help:"Paths to create." type:"path"`
|
|
} `cmd:"" help:"Create directories if they do not already exist."`
|
|
Rmdir struct {
|
|
Paths []string `name:"path" arg:"" help:"Paths to remove." type:"path"`
|
|
} `cmd:"" help:"Remove directories."`
|
|
Read struct {
|
|
Paths []string `name:"path" arg:"" help:"Paths to read." type:"path"`
|
|
} `cmd:"" help:"Read files."`
|
|
} `cmd:""`
|
|
|
|
Tests struct {
|
|
Run TestsRunParams `cmd:"" help:"Run tests."`
|
|
} `cmd:""`
|
|
|
|
Fuzz struct {
|
|
Corpus struct {
|
|
Src string `arg:"" help:"Source, one of: 'seed', 'generated', or collected corpus' directory."`
|
|
Dst string `arg:"" help:"Destination, one of: 'seed', 'generated', or collected corpus' directory."`
|
|
} `cmd:"" help:"Sync fuzz corpora."`
|
|
} `cmd:""`
|
|
}
|
|
|
|
func main() {
|
|
kongCtx := kong.Parse(&cli, kong.DefaultEnvars("ENVTOOL"))
|
|
|
|
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
if t, _ := strconv.ParseBool(os.Getenv("RUNNER_DEBUG")); t {
|
|
cli.Debug = true
|
|
}
|
|
|
|
level := slog.LevelInfo
|
|
if cli.Debug {
|
|
level = slog.LevelDebug
|
|
}
|
|
|
|
opts := &logging.NewHandlerOpts{
|
|
Base: "console",
|
|
Level: level,
|
|
RemoveTime: true,
|
|
RemoveSource: true,
|
|
// the logger handles messages from the `go test` command output
|
|
SkipChecks: true,
|
|
}
|
|
logging.SetupDefault(opts, "")
|
|
logger := slog.Default()
|
|
|
|
cmd := kongCtx.Command()
|
|
logger.Debug(fmt.Sprintf("Command: %q", cmd))
|
|
|
|
var err error
|
|
|
|
switch cmd {
|
|
case "setup":
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
err = setup(ctx, logger)
|
|
|
|
case "package-version":
|
|
err = packageVersion(os.Stdout, versionFile)
|
|
|
|
case "shell mkdir <path>":
|
|
err = shellMkDir(cli.Shell.Mkdir.Paths...)
|
|
case "shell rmdir <path>":
|
|
err = shellRmDir(cli.Shell.Rmdir.Paths...)
|
|
case "shell read <path>":
|
|
err = shellRead(os.Stdout, cli.Shell.Read.Paths...)
|
|
|
|
case "tests run <args>":
|
|
ctx, stop := ctxutil.SigTerm(context.Background())
|
|
defer stop()
|
|
|
|
err = testsRun(ctx, &cli.Tests.Run, logger)
|
|
|
|
case "fuzz corpus <src> <dst>":
|
|
var seedCorpus, generatedCorpus string
|
|
|
|
if seedCorpus, err = os.Getwd(); err != nil {
|
|
logger.LogAttrs(context.Background(), logging.LevelFatal, "Failed to get current directory", logging.Error(err))
|
|
}
|
|
|
|
if generatedCorpus, err = fuzzGeneratedCorpus(); err != nil {
|
|
logger.LogAttrs(context.Background(), logging.LevelFatal, "Failed to generate fuzz corpus", logging.Error(err))
|
|
}
|
|
|
|
var src, dst string
|
|
|
|
switch cli.Fuzz.Corpus.Src {
|
|
case "seed":
|
|
src = seedCorpus
|
|
case "generated":
|
|
src = generatedCorpus
|
|
default:
|
|
if src, err = filepath.Abs(cli.Fuzz.Corpus.Src); err != nil {
|
|
logger.LogAttrs(context.Background(), logging.LevelFatal, "Unknown fuzz corpus source", logging.Error(err))
|
|
}
|
|
}
|
|
|
|
switch cli.Fuzz.Corpus.Dst {
|
|
case "seed":
|
|
// Because we would need to add `/testdata/fuzz` back, and that's not very easy.
|
|
logger.LogAttrs(
|
|
context.Background(),
|
|
logging.LevelFatal,
|
|
"Copying to seed corpus is not supported",
|
|
logging.Error(err),
|
|
)
|
|
case "generated":
|
|
dst = generatedCorpus
|
|
default:
|
|
dst, err = filepath.Abs(cli.Fuzz.Corpus.Dst)
|
|
if err != nil {
|
|
logger.LogAttrs(context.Background(), logging.LevelFatal, "Unknown fuzz corpus destination", logging.Error(err))
|
|
}
|
|
}
|
|
|
|
err = fuzzCopyCorpus(src, dst, logger)
|
|
|
|
default:
|
|
err = fmt.Errorf("unknown command: %s", cmd)
|
|
}
|
|
|
|
if err != nil {
|
|
if cmd == "setup" {
|
|
_ = printDiagnosticData(os.Stderr, err, logger)
|
|
}
|
|
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
logger.Error("Failed", logging.Error(exitErr))
|
|
os.Exit(exitErr.ExitCode())
|
|
}
|
|
|
|
logger.LogAttrs(context.Background(), logging.LevelFatal, "Failed unexpectedly", logging.Error(err))
|
|
}
|
|
}
|