diff --git a/bin_unix.go b/bin_unix.go index 3443d33..427dd74 100644 --- a/bin_unix.go +++ b/bin_unix.go @@ -2,7 +2,4 @@ package k6exec -const ( - k6binary = "k6" - k6temp = "k6-*" -) +const k6binary = "k6" diff --git a/bin_windows.go b/bin_windows.go index 603ca4c..5c1a788 100644 --- a/bin_windows.go +++ b/bin_windows.go @@ -2,7 +2,4 @@ package k6exec -const ( - k6binary = "k6.exe" - k6temp = "k6-*.exe" -) +const k6binary = "k6.exe" diff --git a/build.go b/build.go index e832e55..0ae158b 100644 --- a/build.go +++ b/build.go @@ -15,20 +15,30 @@ import ( "github.com/grafana/k6deps" ) -const platform = runtime.GOOS + "/" + runtime.GOARCH +const ( + platform = runtime.GOOS + "/" + runtime.GOARCH -func depsConvert(deps []*k6deps.Dependency) (string, []k6build.Dependency) { - bdeps := make([]k6build.Dependency, len(deps)-1) + k6module = "k6" +) + +func depsConvert(deps k6deps.Dependencies) (string, []k6build.Dependency) { + bdeps := make([]k6build.Dependency, 0, len(deps)) + k6constraint := "*" + + for _, dep := range deps { + if dep.Name == k6module { + k6constraint = dep.GetConstraints().String() + continue + } - for idx, dep := range deps[1:] { - bdeps[idx] = k6build.Dependency{Name: dep.Name, Constraints: dep.GetConstraints().String()} + bdeps = append(bdeps, k6build.Dependency{Name: dep.Name, Constraints: dep.GetConstraints().String()}) } - return deps[0].GetConstraints().String(), bdeps + return k6constraint, bdeps } -func build(ctx context.Context, deps []*k6deps.Dependency, _ *Options) (*url.URL, error) { - svc, err := k6build.DefaultLocalBuildService() +func build(ctx context.Context, deps k6deps.Dependencies, opts *Options) (*url.URL, error) { + svc, err := newBuildService(ctx, opts) if err != nil { return nil, err } @@ -43,9 +53,56 @@ func build(ctx context.Context, deps []*k6deps.Dependency, _ *Options) (*url.URL return url.Parse(artifact.URL) } +func newBuildService(ctx context.Context, opts *Options) (k6build.BuildService, error) { + if opts.BuildServiceURL != nil { + return newBuildServiceClient(opts) + } + + return newLocalBuildService(ctx, opts) +} + +func newLocalBuildService(ctx context.Context, opts *Options) (k6build.BuildService, error) { + statedir, err := opts.stateSubdir() + if err != nil { + return nil, err + } + + catfile := filepath.Join(statedir, "catalog.json") + + client, err := opts.client() + if err != nil { + return nil, err + } + + if err := download(ctx, opts.catalogURL(), catfile, client); err != nil { + return nil, err + } + + cachedir, err := opts.cacheDir() + if err != nil { + return nil, err + } + + conf := k6build.LocalBuildServiceConfig{ + BuildEnv: map[string]string{"GOWORK": "off"}, + Catalog: catfile, + CopyGoEnv: true, + CacheDir: filepath.Join(cachedir, "build"), + Verbose: opts.verbose(), + } + + return k6build.NewLocalBuildService(ctx, conf) +} + +func newBuildServiceClient(opts *Options) (k6build.BuildService, error) { + return k6build.NewBuildServiceClient(k6build.BuildServiceClientConfig{ + URL: opts.BuildServiceURL.String(), + }) +} + //nolint:forbidigo func download(ctx context.Context, from *url.URL, dest string, client *http.Client) error { - tmp, err := os.CreateTemp(filepath.Dir(dest), k6temp) + tmp, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+"-*") if err != nil { return err } @@ -66,6 +123,11 @@ func download(ctx context.Context, from *url.URL, dest string, client *http.Clie return err } + err = os.Chmod(tmp.Name(), syscall.S_IRUSR|syscall.S_IXUSR) + if err != nil { + return err + } + return os.Rename(tmp.Name(), dest) } @@ -79,16 +141,8 @@ func fileDownload(from *url.URL, dest *os.File) error { defer src.Close() //nolint:errcheck _, err = io.Copy(dest, src) - if err != nil { - return err - } - err = os.Chmod(dest.Name(), syscall.S_IRUSR|syscall.S_IXUSR) - if err != nil { - return err - } - - return nil + return err } //nolint:forbidigo @@ -110,14 +164,6 @@ func httpDownload(ctx context.Context, from *url.URL, dest *os.File, client *htt defer resp.Body.Close() //nolint:errcheck _, err = io.Copy(dest, resp.Body) - if err != nil { - return err - } - - err = os.Chmod(dest.Name(), syscall.S_IRUSR|syscall.S_IXUSR) - if err != nil { - return err - } - return nil + return err } diff --git a/cleanup.go b/cleanup.go new file mode 100644 index 0000000..3ccf859 --- /dev/null +++ b/cleanup.go @@ -0,0 +1,20 @@ +package k6exec + +import ( + "fmt" + "os" +) + +// CleanupState deletes the state directory belonging to the current process. +func CleanupState(opts *Options) error { + dir, err := opts.stateSubdir() + if err != nil { + return fmt.Errorf("%w: %s", ErrState, err.Error()) + } + + if err = os.RemoveAll(dir); err != nil { //nolint:forbidigo + return fmt.Errorf("%w: %s", ErrState, err.Error()) + } + + return nil +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 780b0df..771ade2 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,6 +4,7 @@ package cmd import ( "context" _ "embed" + "net/url" "os" "github.com/grafana/k6deps" @@ -16,6 +17,30 @@ var help string type options struct { k6exec.Options + buildServiceURL string + catalogURL string +} + +func (o *options) postProcess() error { + if len(o.buildServiceURL) > 0 { + val, err := url.Parse(o.buildServiceURL) + if err != nil { + return err + } + + o.BuildServiceURL = val + } + + if len(o.catalogURL) > 0 { + val, err := url.Parse(o.catalogURL) + if err != nil { + return err + } + + o.CatalogURL = val + } + + return nil } // New creates new cobra command for exec command. @@ -31,13 +56,18 @@ func New() *cobra.Command { DisableAutoGenTag: true, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, + PersistentPreRunE: func(_ *cobra.Command, _ []string) error { return opts.postProcess() }, } root.AddCommand(subcommands(&opts.Options)...) flags := root.PersistentFlags() - flags.BoolVar(&opts.ForceUpdate, "force-update", false, "force updating the cached k6 executable") + flags.StringVar(&opts.catalogURL, "extension-catalog", "", "URL of the k6 extension catalog to be used") + flags.StringVar(&opts.buildServiceURL, "build-service", "", "URL of the k6 build service to be used") + flags.BoolVarP(&opts.Verbose, "verbose", "v", false, "enable verbose logging") + + root.MarkFlagsMutuallyExclusive("extension-catalog", "build-service") return root } @@ -51,24 +81,25 @@ func usage(cmd *cobra.Command, args []string) { func exec(sub *cobra.Command, args []string, opts *k6exec.Options) error { var ( - deps k6deps.Dependencies - err error + deps k6deps.Dependencies + err error + dopts k6deps.Options ) if scriptname, hasScript := scriptArg(sub, args); hasScript { - deps, err = k6deps.Analyze(&k6deps.Options{ - Script: k6deps.Source{ - Name: scriptname, - }, - }) - if err != nil { - return err - } + dopts.Script.Name = scriptname + } + + dopts.Manifest.Name = "package.json" + + deps, err = k6deps.Analyze(&dopts) + if err != nil { + return err } args = append([]string{sub.Name()}, args...) - cmd, err := k6exec.Command(context.TODO(), args, deps, opts) + cmd, err := k6exec.Command(context.Background(), args, deps, opts) if err != nil { return err } @@ -77,6 +108,8 @@ func exec(sub *cobra.Command, args []string, opts *k6exec.Options) error { cmd.Stdout = os.Stdout //nolint:forbidigo cmd.Stdin = os.Stdin //nolint:forbidigo + defer k6exec.CleanupState(opts) //nolint:errcheck + return cmd.Run() } diff --git a/command.go b/command.go index d6efdb9..a0b400b 100644 --- a/command.go +++ b/command.go @@ -4,59 +4,38 @@ import ( "context" "errors" "fmt" - "os" "os/exec" "path/filepath" - "syscall" - "github.com/adrg/xdg" "github.com/grafana/k6deps" ) -//nolint:forbidigo -func exists(file string) bool { - _, err := os.Stat(file) - - return err == nil || !errors.Is(err, os.ErrNotExist) -} - // Command returns the exec.Cmd struct to execute k6 with the given dependencies and arguments. func Command(ctx context.Context, args []string, deps k6deps.Dependencies, opts *Options) (*exec.Cmd, error) { - cachedir, err := xdg.CacheFile(opts.appname()) + dir, err := opts.stateSubdir() if err != nil { - return nil, fmt.Errorf("%w: %s", ErrCache, err.Error()) + return nil, fmt.Errorf("%w: %s", ErrState, err.Error()) } - err = os.MkdirAll(cachedir, syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR) //nolint:forbidigo + exe := filepath.Join(dir, k6binary) + + loc, err := build(ctx, deps, opts) if err != nil { - return nil, fmt.Errorf("%w: %s", ErrCache, err.Error()) + return nil, fmt.Errorf("%w: %s", ErrBuild, err.Error()) } - exe := filepath.Join(cachedir, k6binary) - - var mods modules - - if !opts.forceUpdate() && exists(exe) { - mods, err = unmarshalVersionOutput(ctx, exe) - if err != nil { - return nil, fmt.Errorf("%w: %s", ErrCache, err.Error()) - } + client, err := opts.client() + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrDownload, err.Error()) } - if opts.forceUpdate() || !mods.fulfill(deps) { - demands := mods.merge(deps) - - loc, err := build(ctx, demands.Sorted(), opts) - if err != nil { - return nil, fmt.Errorf("%w: %s", ErrBuild, err.Error()) - } - - if err = download(ctx, loc, exe, opts.client()); err != nil { - return nil, fmt.Errorf("%w: %s", ErrDownload, err.Error()) - } + if err = download(ctx, loc, exe, client); err != nil { + return nil, fmt.Errorf("%w: %s", ErrDownload, err.Error()) } - return exec.Command(exe, args...), nil //nolint:gosec + cmd := exec.CommandContext(ctx, exe, args...) //nolint:gosec + + return cmd, nil } var ( @@ -66,4 +45,6 @@ var ( ErrBuild = errors.New("build error") // ErrCache is returned if an error occurs during cache handling. ErrCache = errors.New("cache error") + // ErrState is returned if an error occurs during state handling. + ErrState = errors.New("state error") ) diff --git a/go.mod b/go.mod index f8e51f6..4a2e210 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,25 @@ go 1.22.2 toolchain go1.22.4 require ( - github.com/Masterminds/semver/v3 v3.2.1 github.com/adrg/xdg v0.4.0 github.com/evanw/esbuild v0.21.5 github.com/grafana/clireadme v0.1.0 github.com/grafana/k6build v0.2.0 github.com/grafana/k6deps v0.1.2-0.20240617140502-f1b0dfc93f7f + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/spf13/cobra v1.8.1 golang.org/x/term v0.21.0 ) require ( github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/google/btree v1.1.2 // indirect github.com/grafana/k6catalog v0.1.0 // indirect github.com/grafana/k6foundry v0.1.3 // indirect github.com/grafana/k6pack v0.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/mod v0.18.0 // indirect diff --git a/go.sum b/go.sum index 3e11ef4..79609ac 100644 --- a/go.sum +++ b/go.sum @@ -4,19 +4,18 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/evanw/esbuild v0.21.5 h1:oShm8TT5QUhf6vM7teg0nmd14eHu64dPmVluC2f4DMg= github.com/evanw/esbuild v0.21.5/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grafana/clireadme v0.1.0 h1:KYEYSnYdSzmHf3bufaK6fQZ5j4dzvM/T+G6Ba+qNnAM= github.com/grafana/clireadme v0.1.0/go.mod h1:Wy4KIG2ZBGMYAYyF9l7qAy+yoJVasqk/txsRgoRI3gc= -github.com/grafana/k6build v0.1.5 h1:uhMSujUhLNB3jSs7vfHP6x1yBOq/1fP0TVekibkmgiM= -github.com/grafana/k6build v0.1.5/go.mod h1:DXItIZzDI1gnMOC0+oSE2OsjNJtR4ahLHYC8EQ643T8= github.com/grafana/k6build v0.2.0 h1:4IRinD5iuPW7+XR5590UduPwm1hBAwH2bpdkMADifP8= github.com/grafana/k6build v0.2.0/go.mod h1:DXItIZzDI1gnMOC0+oSE2OsjNJtR4ahLHYC8EQ643T8= github.com/grafana/k6catalog v0.1.0 h1:jLmbmB3EUJ+zyQG3hWy6dWbtMjvTkvJNx1d4LX8it6I= @@ -25,19 +24,19 @@ github.com/grafana/k6deps v0.1.2-0.20240617140502-f1b0dfc93f7f h1:TQE8m+ScEDi4s+ github.com/grafana/k6deps v0.1.2-0.20240617140502-f1b0dfc93f7f/go.mod h1:j8UOs5mZhn5+hpJqDtl5zjYRjMpBKYf+FwaBmHHcfao= github.com/grafana/k6foundry v0.1.3 h1:05sRM5ik+MsZr1tdJR/rTjI8trLpWFbG+vzmnpmsC5g= github.com/grafana/k6foundry v0.1.3/go.mod h1:b6n4InFgXl+3yPobmlyJfcJmLozU9CI9IIUuq8YqEiM= -github.com/grafana/k6pack v0.2.0 h1:Y8udypzuzFdcFMRHnP5VkdjYXWLb/ouKzm4ct3OqPmg= -github.com/grafana/k6pack v0.2.0/go.mod h1:cltO5vmQOObJDtKbRkNVGymk3J+MRhriJyQg1JJzeEc= github.com/grafana/k6pack v0.2.1 h1:S9EkeFuRMnfwP/lHrKnlgctlNDiUKgKU1bEKbIfOUro= github.com/grafana/k6pack v0.2.1/go.mod h1:BEy4y0GE+gXbdp8EldJGXd1g1Py3wBBxDE2AwzHsMxI= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/module.go b/module.go deleted file mode 100644 index 2c474a3..0000000 --- a/module.go +++ /dev/null @@ -1,105 +0,0 @@ -package k6exec - -import ( - "context" - "os/exec" - "regexp" - - "github.com/Masterminds/semver/v3" - "github.com/grafana/k6deps" -) - -type module struct { - name string - version *semver.Version -} - -type modules map[string]*module - -func (mods *modules) unmarshalVersionOutput(text []byte) error { - *mods = make(modules) - - if allmatch := reK6.FindAllSubmatch(text, -1); allmatch != nil { - match := allmatch[0] - - version, err := semver.NewVersion(string(match[idxK6Version])) - if err != nil { - return err - } - - (*mods)[k6module] = &module{name: k6module, version: version} - } - - for _, match := range reExtension.FindAllSubmatch(text, -1) { - version, err := semver.NewVersion(string(match[idxExtVersion])) - if err != nil { - return err - } - - name := string(match[idxExtName]) - - (*mods)[name] = &module{name: name, version: version} - } - - return nil -} - -func (mods modules) fulfill(deps k6deps.Dependencies) bool { - for _, dep := range deps { - mod, found := mods[dep.Name] - if !found || (dep.Constraints != nil && !dep.Constraints.Check(mod.version)) { - return false - } - } - - return true -} - -func (mods modules) merge(deps k6deps.Dependencies) k6deps.Dependencies { - merged := make(k6deps.Dependencies, len(deps)) - - for name, dep := range deps { - merged[name] = dep - } - - for name := range mods { - if _, found := merged[name]; !found { - merged[name] = &k6deps.Dependency{Name: name} - } - } - - if _, found := merged[k6module]; !found { - merged[k6module] = &k6deps.Dependency{Name: k6module} - } - - return merged -} - -func unmarshalVersionOutput(ctx context.Context, cmd string) (modules, error) { - c := exec.CommandContext(ctx, cmd, "version") - - out, err := c.CombinedOutput() - if err != nil { - return nil, err - } - - var mods modules - - err = mods.unmarshalVersionOutput(out) - if err != nil { - return nil, err - } - - return mods, nil -} - -//nolint:gochecknoglobals -var ( - reK6 = regexp.MustCompile(`k6 (?P[^ ]+) .*`) - reExtension = regexp.MustCompile(` (?P[^ ]+) (?P[^,]+), (?P[^ ]+) \[([^\]]+)\]`) - idxK6Version = reK6.SubexpIndex("k6Version") - idxExtVersion = reExtension.SubexpIndex("extVersion") - idxExtName = reExtension.SubexpIndex("extName") -) - -const k6module = "k6" diff --git a/options.go b/options.go index 91acb50..f32991d 100644 --- a/options.go +++ b/options.go @@ -1,9 +1,17 @@ package k6exec import ( + "fmt" "net/http" + "net/url" "os" "path/filepath" + "strconv" + "syscall" + + "github.com/adrg/xdg" + "github.com/gregjones/httpcache" + "github.com/gregjones/httpcache/diskcache" ) // Options contains the optional parameters of the Command function. @@ -11,18 +19,32 @@ type Options struct { // AppName contains the name of the application. It is used to define the default value of CacheDir. // If empty, it defaults to os.Args[0]. AppName string - // CacheDir specifies the name of the directory where the k6 binary can be cached. + // CacheDir specifies the name of the directory where the cacheable files can be cached. // Its default is determined based on the XDG Base Directory Specification. // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html CacheDir string - // ForceUpdate can be used to unconditionally update the cached k6 binary. - // By setting its value to true, the k6 binary can be forced to update. - ForceUpdate bool + // StateDir specifies the name of the directory where the k6 running state is stored, + // including the k6 binary and extension catalog. Each execution has a sub-directory, + // which is deleted after successful execution. + // Its default is determined based on the XDG Base Directory Specification. + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + StateDir string // Client is used during HTTP communication with the build service. // If absent, http.DefaultClient will be used. Client *http.Client + // CatalogURL contains the URL of the k6 extension catalog to be used. + // If absent, DefaultCatalogURL will be used. + CatalogURL *url.URL + // Verbose flag enables verbose logging. + Verbose bool + // BuildServiceURL contains the URL of the k6 build service to be used. + // If the value is not nil, the k6 binary is built using the build service instead of the local build. + BuildServiceURL *url.URL } +// DefaultCatalogURL contains the address of the default k6 extension catalog. +const DefaultCatalogURL = "https://grafana.github.io/k6-extension-catalogs/registered.json" + func (o *Options) appname() string { if o != nil && len(o.AppName) > 0 { return o.AppName @@ -31,14 +53,94 @@ func (o *Options) appname() string { return filepath.Base(os.Args[0]) //nolint:forbidigo } -func (o *Options) client() *http.Client { +func (o *Options) client() (*http.Client, error) { if o != nil && o.Client != nil { - return o.Client + return o.Client, nil + } + + cachedir, err := o.cacheDir() + if err != nil { + return nil, err + } + + dir := filepath.Join(cachedir, "http") + + err = os.MkdirAll(dir, syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR) //nolint:forbidigo + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrCache, err.Error()) + } + + transport := httpcache.NewTransport(diskcache.New(dir)) + + return &http.Client{Transport: transport}, nil +} + +func (o *Options) catalogURL() *url.URL { + if o != nil && o.CatalogURL != nil { + return o.CatalogURL + } + + loc, _ := url.Parse(DefaultCatalogURL) + + return loc +} + +func (o *Options) xdgDir(option string, xdgfunc func(string) (string, error), e error) (string, error) { + var xdgdir string + + if o != nil && len(option) != 0 { + xdgdir = option + } else { + dir, err := xdgfunc(o.appname()) + if err != nil { + return "", fmt.Errorf("%w: %s", e, err.Error()) + } + + xdgdir = dir + } + + err := os.MkdirAll(xdgdir, syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR) //nolint:forbidigo + if err != nil { + return "", fmt.Errorf("%w: %s", e, err.Error()) + } + + return xdgdir, nil +} + +func (o *Options) cacheDir() (string, error) { + var option string + if o != nil { + option = o.CacheDir + } + + return o.xdgDir(option, xdg.CacheFile, ErrCache) +} + +func (o *Options) stateDir() (string, error) { + var option string + if o != nil { + option = o.StateDir + } + + return o.xdgDir(option, xdg.StateFile, ErrState) +} + +func (o *Options) stateSubdir() (string, error) { + dir, err := o.stateDir() + if err != nil { + return "", err + } + + dir = filepath.Join(dir, strconv.Itoa(os.Getpid())) //nolint:forbidigo + + err = os.MkdirAll(dir, syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR) //nolint:forbidigo + if err != nil { + return "", err } - return http.DefaultClient + return dir, nil } -func (o *Options) forceUpdate() bool { - return o != nil && o.ForceUpdate +func (o *Options) verbose() bool { + return o != nil && o.Verbose }