mirror of https://github.com/coder/coder.git
feat: Use open-source Terraform Provider (#403)
This removes our internal Terraform Provider, and opens it to the world!
This commit is contained in:
parent
bf0ae8f573
commit
18c929c8ab
12
Makefile
12
Makefile
|
@ -12,12 +12,7 @@ bin/coderd:
|
|||
go build -o bin/coderd cmd/coderd/main.go
|
||||
.PHONY: bin/coderd
|
||||
|
||||
bin/terraform-provider-coder:
|
||||
mkdir -p bin
|
||||
go build -o bin/terraform-provider-coder cmd/terraform-provider-coder/main.go
|
||||
.PHONY: bin/terraform-provider-coder
|
||||
|
||||
build: site/out bin/coder bin/coderd bin/terraform-provider-coder
|
||||
build: site/out bin/coder bin/coderd
|
||||
.PHONY: build
|
||||
|
||||
# Runs migrations to output a dump of the database.
|
||||
|
@ -66,11 +61,6 @@ install:
|
|||
@echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)"
|
||||
.PHONY: install
|
||||
|
||||
install/terraform-provider-coder: bin/terraform-provider-coder
|
||||
$(eval OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH))
|
||||
mkdir -p ~/.terraform.d/plugins/coder.com/internal/coder/0.2/$(OS_ARCH)
|
||||
cp bin/terraform-provider-coder ~/.terraform.d/plugins/coder.com/internal/coder/0.2/$(OS_ARCH)
|
||||
|
||||
peerbroker/proto: peerbroker/proto/peerbroker.proto
|
||||
protoc \
|
||||
--go_out=. \
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
|
||||
|
||||
"github.com/coder/coder/provisioner/terraform/provider"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeOpts{
|
||||
ProviderFunc: provider.New,
|
||||
})
|
||||
}
|
2
go.mod
2
go.mod
|
@ -57,7 +57,6 @@ require (
|
|||
github.com/tabbed/pqtype v0.1.1
|
||||
github.com/unrolled/secure v1.10.0
|
||||
github.com/xlab/treeprint v1.1.0
|
||||
go.opencensus.io v0.23.0
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/goleak v1.1.12
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
||||
|
@ -151,6 +150,7 @@ require (
|
|||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/zclconf/go-cty v1.10.0 // indirect
|
||||
github.com/zeebo/errs v1.2.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
URL *url.URL
|
||||
}
|
||||
|
||||
// New returns a new Terraform provider.
|
||||
func New() *schema.Provider {
|
||||
return &schema.Provider{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"url": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
// The "CODER_URL" environment variable is used by default
|
||||
// as the Access URL when generating scripts.
|
||||
DefaultFunc: schema.EnvDefaultFunc("CODER_URL", ""),
|
||||
ValidateFunc: func(i interface{}, s string) ([]string, []error) {
|
||||
_, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, []error{err}
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
ConfigureContextFunc: func(c context.Context, resourceData *schema.ResourceData) (interface{}, diag.Diagnostics) {
|
||||
rawURL, ok := resourceData.Get("url").(string)
|
||||
if !ok {
|
||||
return nil, diag.Errorf("unexpected type %q for url", reflect.TypeOf(resourceData.Get("url")).String())
|
||||
}
|
||||
if rawURL == "" {
|
||||
return nil, diag.Errorf("CODER_URL must not be empty; got %q", rawURL)
|
||||
}
|
||||
parsed, err := url.Parse(resourceData.Get("url").(string))
|
||||
if err != nil {
|
||||
return nil, diag.FromErr(err)
|
||||
}
|
||||
return config{
|
||||
URL: parsed,
|
||||
}, nil
|
||||
},
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"coder_workspace": {
|
||||
Description: "TODO",
|
||||
ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
|
||||
rd.SetId(uuid.NewString())
|
||||
return nil
|
||||
},
|
||||
Schema: map[string]*schema.Schema{
|
||||
"transition": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "TODO",
|
||||
DefaultFunc: schema.EnvDefaultFunc("CODER_WORKSPACE_TRANSITION", ""),
|
||||
ValidateFunc: validation.StringInSlice([]string{string(database.WorkspaceTransitionStart), string(database.WorkspaceTransitionStop)}, false),
|
||||
},
|
||||
},
|
||||
},
|
||||
"coder_agent_script": {
|
||||
Description: "TODO",
|
||||
ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
|
||||
config, valid := i.(config)
|
||||
if !valid {
|
||||
return diag.Errorf("config was unexpected type %q", reflect.TypeOf(i).String())
|
||||
}
|
||||
operatingSystem, valid := resourceData.Get("os").(string)
|
||||
if !valid {
|
||||
return diag.Errorf("os was unexpected type %q", reflect.TypeOf(resourceData.Get("os")))
|
||||
}
|
||||
arch, valid := resourceData.Get("arch").(string)
|
||||
if !valid {
|
||||
return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch")))
|
||||
}
|
||||
script, err := provisionersdk.AgentScript(config.URL, operatingSystem, arch)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
err = resourceData.Set("value", script)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
resourceData.SetId(strings.Join([]string{operatingSystem, arch}, "_"))
|
||||
return nil
|
||||
},
|
||||
Schema: map[string]*schema.Schema{
|
||||
"os": {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false),
|
||||
},
|
||||
"arch": {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ValidateFunc: validation.StringInSlice([]string{"amd64"}, false),
|
||||
},
|
||||
"value": {
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"coder_agent": {
|
||||
Description: "TODO",
|
||||
CreateContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
|
||||
// This should be a real authentication token!
|
||||
rd.SetId(uuid.NewString())
|
||||
err := rd.Set("token", uuid.NewString())
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
|
||||
return nil
|
||||
},
|
||||
DeleteContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
|
||||
return nil
|
||||
},
|
||||
Schema: map[string]*schema.Schema{
|
||||
"auth": {
|
||||
ForceNew: true,
|
||||
Description: "TODO",
|
||||
Type: schema.TypeList,
|
||||
Optional: true,
|
||||
MaxItems: 1,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"type": {
|
||||
ForceNew: true,
|
||||
Description: "TODO",
|
||||
Optional: true,
|
||||
Type: schema.TypeString,
|
||||
ValidateFunc: validation.StringInSlice([]string{"google-instance-identity"}, false),
|
||||
},
|
||||
"instance_id": {
|
||||
ForceNew: true,
|
||||
Description: "TODO",
|
||||
Optional: true,
|
||||
Type: schema.TypeString,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"env": {
|
||||
ForceNew: true,
|
||||
Description: "TODO",
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
},
|
||||
"startup_script": {
|
||||
ForceNew: true,
|
||||
Description: "TODO",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
"token": {
|
||||
ForceNew: true,
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
package provider_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/provisioner/terraform/provider"
|
||||
)
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
tfProvider := provider.New()
|
||||
err := tfProvider.InternalValidate()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWorkspace(t *testing.T) {
|
||||
t.Parallel()
|
||||
resource.Test(t, resource.TestCase{
|
||||
Providers: map[string]*schema.Provider{
|
||||
"coder": provider.New(),
|
||||
},
|
||||
IsUnitTest: true,
|
||||
Steps: []resource.TestStep{{
|
||||
Config: `
|
||||
provider "coder" {
|
||||
url = "https://example.com"
|
||||
}
|
||||
data "coder_workspace" "me" {
|
||||
transition = "start"
|
||||
}`,
|
||||
Check: func(state *terraform.State) error {
|
||||
require.Len(t, state.Modules, 1)
|
||||
require.Len(t, state.Modules[0].Resources, 1)
|
||||
resource := state.Modules[0].Resources["data.coder_workspace.me"]
|
||||
require.NotNil(t, resource)
|
||||
value := resource.Primary.Attributes["transition"]
|
||||
require.NotNil(t, value)
|
||||
t.Log(value)
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgentScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
resource.Test(t, resource.TestCase{
|
||||
Providers: map[string]*schema.Provider{
|
||||
"coder": provider.New(),
|
||||
},
|
||||
IsUnitTest: true,
|
||||
Steps: []resource.TestStep{{
|
||||
Config: `
|
||||
provider "coder" {
|
||||
url = "https://example.com"
|
||||
}
|
||||
data "coder_agent_script" "new" {
|
||||
arch = "amd64"
|
||||
os = "linux"
|
||||
}`,
|
||||
Check: func(state *terraform.State) error {
|
||||
require.Len(t, state.Modules, 1)
|
||||
require.Len(t, state.Modules[0].Resources, 1)
|
||||
resource := state.Modules[0].Resources["data.coder_agent_script.new"]
|
||||
require.NotNil(t, resource)
|
||||
value := resource.Primary.Attributes["value"]
|
||||
require.NotNil(t, value)
|
||||
t.Log(value)
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resource.Test(t, resource.TestCase{
|
||||
Providers: map[string]*schema.Provider{
|
||||
"coder": provider.New(),
|
||||
},
|
||||
IsUnitTest: true,
|
||||
Steps: []resource.TestStep{{
|
||||
Config: `
|
||||
provider "coder" {
|
||||
url = "https://example.com"
|
||||
}
|
||||
resource "coder_agent" "new" {}`,
|
||||
Check: func(state *terraform.State) error {
|
||||
require.Len(t, state.Modules, 1)
|
||||
require.Len(t, state.Modules[0].Resources, 1)
|
||||
resource := state.Modules[0].Resources["coder_agent.new"]
|
||||
require.NotNil(t, resource)
|
||||
require.NotNil(t, resource.Primary.Attributes["token"])
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Filled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resource.Test(t, resource.TestCase{
|
||||
Providers: map[string]*schema.Provider{
|
||||
"coder": provider.New(),
|
||||
},
|
||||
IsUnitTest: true,
|
||||
Steps: []resource.TestStep{{
|
||||
Config: `
|
||||
provider "coder" {
|
||||
url = "https://example.com"
|
||||
}
|
||||
resource "coder_agent" "new" {
|
||||
auth {
|
||||
type = "google-instance-identity"
|
||||
instance_id = "instance"
|
||||
}
|
||||
env = {
|
||||
hi = "test"
|
||||
}
|
||||
startup_script = "echo test"
|
||||
}`,
|
||||
Check: func(state *terraform.State) error {
|
||||
require.Len(t, state.Modules, 1)
|
||||
require.Len(t, state.Modules[0].Resources, 1)
|
||||
resource := state.Modules[0].Resources["coder_agent.new"]
|
||||
require.NotNil(t, resource)
|
||||
for _, key := range []string{
|
||||
"token",
|
||||
"auth.0.type",
|
||||
"auth.0.instance_id",
|
||||
"env.hi",
|
||||
"startup_script",
|
||||
} {
|
||||
value := resource.Primary.Attributes[key]
|
||||
t.Log(fmt.Sprintf("%q = %q", key, value))
|
||||
require.NotNil(t, value)
|
||||
require.Greater(t, len(value), 0)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
})
|
||||
})
|
||||
}
|
|
@ -5,11 +5,8 @@ package terraform_test
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -25,27 +22,12 @@ import (
|
|||
func TestProvision(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Build and output the Terraform Provider that is consumed for these tests.
|
||||
homeDir, err := os.UserHomeDir()
|
||||
require.NoError(t, err)
|
||||
providerDest := filepath.Join(homeDir, ".terraform.d", "plugins", "coder.com", "internal", "coder", "0.0.1", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
|
||||
err = os.MkdirAll(providerDest, 0700)
|
||||
require.NoError(t, err)
|
||||
//nolint:dogsled
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
providerSrc := filepath.Join(filepath.Dir(filename), "..", "..", "cmd", "terraform-provider-coder")
|
||||
output, err := exec.Command("go", "build", "-o", providerDest, providerSrc).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Log(string(output))
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
provider := `
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder.com/internal/coder"
|
||||
version = "0.0.1"
|
||||
source = "coder/coder"
|
||||
version = "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
package provisionersdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
// A mapping of operating-system ($GOOS) to architecture ($GOARCH)
|
||||
// to agent install and run script. ${DOWNLOAD_URL} is replaced
|
||||
// with strings.ReplaceAll() when being consumed.
|
||||
agentScript = map[string]map[string]string{
|
||||
agentScripts = map[string]map[string]string{
|
||||
"windows": {
|
||||
"amd64": `
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
$ErrorActionPreference = "Stop"
|
||||
Invoke-WebRequest -Uri ${DOWNLOAD_URL} -OutFile $env:TEMP\coder.exe
|
||||
Invoke-WebRequest -Uri ${ACCESS_URL}/bin/coder-windows-amd64 -OutFile $env:TEMP\coder.exe
|
||||
$env:CODER_URL = "${ACCESS_URL}"
|
||||
Start-Process -FilePath $env:TEMP\coder.exe workspaces agent
|
||||
`,
|
||||
|
@ -27,7 +21,7 @@ Start-Process -FilePath $env:TEMP\coder.exe workspaces agent
|
|||
#!/usr/bin/env sh
|
||||
set -eu pipefail
|
||||
BINARY_LOCATION=$(mktemp -d)/coder
|
||||
curl -fsSL ${DOWNLOAD_URL} -o $BINARY_LOCATION
|
||||
curl -fsSL ${ACCESS_URL}/bin/coder-linux-amd64 -o $BINARY_LOCATION
|
||||
chmod +x $BINARY_LOCATION
|
||||
export CODER_URL="${ACCESS_URL}"
|
||||
exec $BINARY_LOCATION agent
|
||||
|
@ -38,7 +32,7 @@ exec $BINARY_LOCATION agent
|
|||
#!/usr/bin/env sh
|
||||
set -eu pipefail
|
||||
BINARY_LOCATION=$(mktemp -d)/coder
|
||||
curl -fsSL ${DOWNLOAD_URL} -o $BINARY_LOCATION
|
||||
curl -fsSL ${ACCESS_URL}/bin/coder-darwin-amd64 -o $BINARY_LOCATION
|
||||
chmod +x $BINARY_LOCATION
|
||||
export CODER_URL="${ACCESS_URL}"
|
||||
exec $BINARY_LOCATION agent
|
||||
|
@ -47,35 +41,15 @@ exec $BINARY_LOCATION agent
|
|||
}
|
||||
)
|
||||
|
||||
// AgentScript returns an installation script for the specified operating system
|
||||
// and architecture.
|
||||
func AgentScript(coderURL *url.URL, operatingSystem, architecture string) (string, error) {
|
||||
architectures, exists := agentScript[operatingSystem]
|
||||
if !exists {
|
||||
list := []string{}
|
||||
for key := range agentScript {
|
||||
list = append(list, key)
|
||||
// AgentScriptEnv returns a key-pair of scripts that are consumed
|
||||
// by the Coder Terraform Provider. See:
|
||||
// https://github.com/coder/terraform-provider-coder/blob/main/internal/provider/provider.go#L97
|
||||
func AgentScriptEnv() map[string]string {
|
||||
env := map[string]string{}
|
||||
for operatingSystem, scripts := range agentScripts {
|
||||
for architecture, script := range scripts {
|
||||
env[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", operatingSystem, architecture)] = script
|
||||
}
|
||||
return "", xerrors.Errorf("operating system %q not supported. must be in: %v", operatingSystem, list)
|
||||
}
|
||||
script, exists := architectures[architecture]
|
||||
if !exists {
|
||||
list := []string{}
|
||||
for key := range architectures {
|
||||
list = append(list, key)
|
||||
}
|
||||
return "", xerrors.Errorf("architecture %q not supported for %q. must be in: %v", architecture, operatingSystem, list)
|
||||
}
|
||||
downloadURL, err := coderURL.Parse(fmt.Sprintf("/bin/coder-%s-%s", operatingSystem, architecture))
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("parse download url: %w", err)
|
||||
}
|
||||
accessURL, err := coderURL.Parse("/")
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("parse access url: %w", err)
|
||||
}
|
||||
return strings.NewReplacer(
|
||||
"${DOWNLOAD_URL}", downloadURL.String(),
|
||||
"${ACCESS_URL}", accessURL.String(),
|
||||
).Replace(script), nil
|
||||
return env
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package provisionersdk_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -37,9 +38,13 @@ func TestAgentScript(t *testing.T) {
|
|||
t.Cleanup(srv.Close)
|
||||
srvURL, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
script, err := provisionersdk.AgentScript(srvURL, runtime.GOOS, runtime.GOARCH)
|
||||
require.NoError(t, err)
|
||||
|
||||
script, exists := provisionersdk.AgentScriptEnv()[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", runtime.GOOS, runtime.GOARCH)]
|
||||
if !exists {
|
||||
t.Skip("Agent not supported...")
|
||||
return
|
||||
}
|
||||
script = strings.ReplaceAll(script, "${ACCESS_URL}", srvURL.String())
|
||||
output, err := exec.Command("sh", "-c", script).CombinedOutput()
|
||||
t.Log(string(output))
|
||||
require.NoError(t, err)
|
||||
|
@ -47,16 +52,4 @@ func TestAgentScript(t *testing.T) {
|
|||
// as the response to executing our script.
|
||||
require.Equal(t, "agent", strings.TrimSpace(string(output)))
|
||||
})
|
||||
|
||||
t.Run("UnsupportedOS", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := provisionersdk.AgentScript(nil, "unsupported", "")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("UnsupportedArch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := provisionersdk.AgentScript(nil, runtime.GOOS, "unsupported")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue