diff --git a/.drone.yml b/.drone.yml index b7e057c..d48c4f4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,21 +6,21 @@ name: checkup steps: - name: test-mysql - image: golang:1.14 + image: golang:1.26.4 pull: always commands: - make test-mysql - make build-mysql - name: test-postgres - image: golang:1.14 + image: golang:1.26.4 pull: always commands: - make test-postgres - make build-postgres - name: test-sqlite3 - image: golang:1.14 + image: golang:1.26.4 pull: always commands: - make test-sqlite3 diff --git a/.travis.yml b/.travis.yml index d596271..64a5336 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: go go: -- 1.14 \ No newline at end of file +- 1.26.4 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index eddee94..816ce50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.14-alpine as builder +FROM golang:1.26.4-alpine as builder ENV CGO_ENABLED=0 diff --git a/check/exec/exec_test.go b/check/exec/exec_test.go index ebf7875..316bd15 100644 --- a/check/exec/exec_test.go +++ b/check/exec/exec_test.go @@ -5,7 +5,7 @@ import ( ) func TestChecker(t *testing.T) { - assert := func(ok bool, format string, args ...interface{}) { + assert := func(ok bool, format string, args ...any) { if !ok { t.Errorf(format, args...) } diff --git a/check/http/http.go b/check/http/http.go index f9b6b5c..5cddd85 100644 --- a/check/http/http.go +++ b/check/http/http.go @@ -3,7 +3,8 @@ package http import ( "encoding/json" "fmt" - "io/ioutil" + "io" + "net" "net/http" "strings" @@ -199,7 +200,7 @@ func (c Checker) checkDown(resp *http.Response) error { if c.MustContain == "" && c.MustNotContain == "" { return nil } - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("reading response body: %w", err) } diff --git a/check/tcp/tcp.go b/check/tcp/tcp.go index 97cd804..f082941 100644 --- a/check/tcp/tcp.go +++ b/check/tcp/tcp.go @@ -6,7 +6,8 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "os" + "net" "time" @@ -109,7 +110,7 @@ func (c Checker) doChecks() types.Attempts { var tlsConfig tls.Config tlsConfig.InsecureSkipVerify = c.TLSSkipVerify if c.TLSCAFile != "" { - rootPEM, err := ioutil.ReadFile(c.TLSCAFile) + rootPEM, err := os.ReadFile(c.TLSCAFile) if err != nil || rootPEM == nil { return nil, errReadingRootCert } diff --git a/check/tls/tls.go b/check/tls/tls.go index fbfd3e3..c66b7c3 100644 --- a/check/tls/tls.go +++ b/check/tls/tls.go @@ -5,7 +5,8 @@ import ( "crypto/x509" "encoding/json" "fmt" - "io/ioutil" + "os" + "net" "time" @@ -18,11 +19,11 @@ const Type = "tls" // Checker implements a Checker for TLS endpoints. // // TODO: Implement more checks on the certificate and TLS configuration. -// - Cipher suites -// - Protocol versions -// - OCSP stapling -// - Multiple SNIs -// - Other things that you might see at SSL Labs or other TLS health checks +// - Cipher suites +// - Protocol versions +// - OCSP stapling +// - Multiple SNIs +// - Other things that you might see at SSL Labs or other TLS health checks type Checker struct { // Name is the name of the endpoint. Name string `json:"endpoint_name"` @@ -92,7 +93,7 @@ func (c Checker) Check() (types.Result, error) { c.tlsConfig.RootCAs = x509.NewCertPool() } for _, fname := range c.TrustedRoots { - pemData, err := ioutil.ReadFile(fname) + pemData, err := os.ReadFile(fname) if err != nil { return types.Result{}, fmt.Errorf("error loading file: %w", err) } diff --git a/check/tls/tls_test.go b/check/tls/tls_test.go index 1fd950a..b388fc7 100644 --- a/check/tls/tls_test.go +++ b/check/tls/tls_test.go @@ -130,7 +130,7 @@ func TestChecker(t *testing.T) { func makeSelfSignedCert(hostname, keyType string, validity time.Duration) (tls.Certificate, error) { // start by generating private key - var privKey interface{} + var privKey any var err error switch keyType { case "", "ec256": @@ -168,7 +168,7 @@ func makeSelfSignedCert(hostname, keyType string, validity time.Duration) (tls.C } cert.DNSNames = append(cert.DNSNames, hostname) - publicKey := func(privKey interface{}) interface{} { + publicKey := func(privKey any) any { switch k := privKey.(type) { case *rsa.PrivateKey: return &k.PublicKey diff --git a/checkup.go b/checkup.go index 0103ef1..72e18ad 100644 --- a/checkup.go +++ b/checkup.go @@ -30,7 +30,7 @@ type Checkup struct { // Useful if wanting to perform distributed check // "at the same time" even if they might actually // be a few milliseconds or seconds apart. - Timestamp time.Time `json:"timestamp,omitempty"` + Timestamp time.Time `json:"timestamp"` // Storage is the storage mechanism for saving the // results of checks. Required if calling Store(). @@ -148,7 +148,7 @@ func (c Checkup) MarshalJSON() ([]byte, error) { // handling; unfortunately this has to mimic c's definition. easy := struct { ConcurrentChecks int `json:"concurrent_checks,omitempty"` - Timestamp time.Time `json:"timestamp,omitempty"` + Timestamp time.Time `json:"timestamp"` }{ ConcurrentChecks: c.ConcurrentChecks, Timestamp: c.Timestamp, @@ -175,7 +175,7 @@ func (c Checkup) MarshalJSON() ([]byte, error) { if err != nil { return result, err } - chb = []byte(fmt.Sprintf(`{"type":"%s",%s`, ch.Type(), string(chb[1:]))) + chb = fmt.Appendf(nil, `{"type":"%s",%s`, ch.Type(), string(chb[1:])) checkers = append(checkers, chb) } @@ -192,7 +192,7 @@ func (c Checkup) MarshalJSON() ([]byte, error) { if err != nil { return result, err } - sb = []byte(fmt.Sprintf(`{"type":"%s",%s`, c.Storage.Type(), string(sb[1:]))) + sb = fmt.Appendf(nil, `{"type":"%s",%s`, c.Storage.Type(), string(sb[1:])) wrap("storage", sb) } @@ -205,7 +205,7 @@ func (c Checkup) MarshalJSON() ([]byte, error) { return result, err } - chb = []byte(fmt.Sprintf(`{"type":"%s",%s`, ch.Type(), string(chb[1:]))) + chb = fmt.Appendf(nil, `{"type":"%s",%s`, ch.Type(), string(chb[1:])) checkers = append(checkers, chb) } diff --git a/checkup_test.go b/checkup_test.go index 6f3cfe5..5d244b8 100644 --- a/checkup_test.go +++ b/checkup_test.go @@ -3,7 +3,8 @@ package checkup import ( "bytes" "errors" - "io/ioutil" + "os" + "sync" "testing" "time" @@ -182,7 +183,7 @@ func TestJSON(t *testing.T) { testConfig = "testdata/config.json" ) - jsonBytes, err := ioutil.ReadFile(testConfig) + jsonBytes, err := os.ReadFile(testConfig) if err != nil { t.Fatalf("Error reading config file: %s", testConfig) } diff --git a/cmd/root.go b/cmd/root.go index 9b94cc5..c2c9853 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,7 +3,7 @@ package cmd import ( "encoding/json" "fmt" - "io/ioutil" + "log" "os" @@ -67,7 +67,7 @@ store the results of the check, use --store.`, } func loadCheckup() checkup.Checkup { - configBytes, err := ioutil.ReadFile(configFile) + configBytes, err := os.ReadFile(configFile) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index a8e0166..6762f6b 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,13 @@ module github.com/sourcegraph/checkup -go 1.13 +go 1.26.4 require ( github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 github.com/aws/aws-sdk-go v1.30.7 - github.com/elazarl/goproxy v0.0.0-20200315184450-1f3cb6622dad // indirect github.com/fatih/color v1.9.0 github.com/go-sql-driver/mysql v1.5.0 github.com/google/go-github v17.0.0+incompatible - github.com/google/go-querystring v1.0.0 // indirect github.com/gregdel/pushover v0.0.0-20200416074932-c8ad547caed4 github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.3.0 @@ -17,12 +15,28 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/microsoft/ApplicationInsights-Go v0.4.3 github.com/miekg/dns v1.1.29 + github.com/spf13/cobra v0.0.7 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df +) + +require ( + code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect + github.com/elazarl/goproxy v0.0.0-20200315184450-1f3cb6622dad // indirect + github.com/go-chi/chi v4.0.0+incompatible // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.3.0 // indirect + github.com/mailru/easyjson v0.7.0 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.11 // indirect github.com/parnurzeal/gorequest v0.2.16 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/satori/go.uuid v1.2.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect - github.com/spf13/cobra v0.0.7 + github.com/spf13/pflag v1.0.3 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + golang.org/x/sys v0.28.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df moul.io/http2curl v1.0.0 // indirect ) diff --git a/notifier/discord/discord.go b/notifier/discord/discord.go index f90525b..e252624 100644 --- a/notifier/discord/discord.go +++ b/notifier/discord/discord.go @@ -5,7 +5,8 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" + "log" "net/http" "strings" @@ -97,7 +98,7 @@ func (s Notifier) Send(result types.Result) error { defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Println("discord: error reading response body", err) } diff --git a/storage/appinsights/appinsights.go b/storage/appinsights/appinsights.go index 6d97214..2685d4b 100644 --- a/storage/appinsights/appinsights.go +++ b/storage/appinsights/appinsights.go @@ -3,6 +3,7 @@ package appinsights import ( "encoding/json" "fmt" + "maps" "strconv" "time" @@ -92,9 +93,7 @@ func (Storage) Type() string { func (c Storage) Store(results []types.Result) error { c.telemetryConfig.InstrumentationKey = c.InstrumentationKey c.client = appinsights.NewTelemetryClientFromConfig(c.telemetryConfig) - for k, v := range c.Tags { - c.client.Context().CommonProperties[k] = v - } + maps.Copy(c.client.Context().CommonProperties, c.Tags) for _, result := range results { c.send(result) } diff --git a/storage/appinsights/appinsights_test.go b/storage/appinsights/appinsights_test.go index 0f6c0b9..402505d 100644 --- a/storage/appinsights/appinsights_test.go +++ b/storage/appinsights/appinsights_test.go @@ -5,7 +5,8 @@ import ( "compress/gzip" "encoding/json" "fmt" - "io/ioutil" + "io" + "net/http" "net/http/httptest" "testing" @@ -124,18 +125,18 @@ func setup(delay time.Duration, retries int, interval int, timeout int, results req, err := gzip.NewReader(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf("gzip NewReader: %v", err))) + w.Write(fmt.Appendf(nil, "gzip NewReader: %v", err)) } - b, _ := ioutil.ReadAll(req) + b, _ := io.ReadAll(req) parsed, err := parsePayload(b) for i, j := range parsed { - data := j["data"].(map[string]interface{}) - baseData := data["baseData"].(map[string]interface{}) + data := j["data"].(map[string]any) + baseData := data["baseData"].(map[string]any) got, ok := baseData["name"].(string) if !ok || got != results[i].Title { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf("Expected test result name to be '%s', but got '%s'", results[i].Title, got))) + w.Write(fmt.Appendf(nil, "Expected test result name to be '%s', but got '%s'", results[i].Title, got)) } } })) @@ -161,14 +162,14 @@ func setup(delay time.Duration, retries int, interval int, timeout int, results } // Ref: https://github.com/microsoft/ApplicationInsights-Go/blob/master/appinsights/jsonserializer_test.go -func parsePayload(payload []byte) (result []map[string]interface{}, err error) { - for _, item := range bytes.Split(payload, []byte("\n")) { +func parsePayload(payload []byte) (result []map[string]any, err error) { + for item := range bytes.SplitSeq(payload, []byte("\n")) { if len(item) == 0 { continue } decoder := json.NewDecoder(bytes.NewReader(item)) - msg := make(map[string]interface{}) + msg := make(map[string]any) if err := decoder.Decode(&msg); err == nil { result = append(result, msg) } else { diff --git a/storage/fs/fs_test.go b/storage/fs/fs_test.go index 822919b..4fc1e75 100644 --- a/storage/fs/fs_test.go +++ b/storage/fs/fs_test.go @@ -2,7 +2,6 @@ package fs import ( "bytes" - "io/ioutil" "os" "path/filepath" "testing" @@ -15,7 +14,7 @@ func TestStorage(t *testing.T) { results := []types.Result{{Title: "Testing"}} resultsBytes := []byte(`[{"title":"Testing"}]` + "\n") - dir, err := ioutil.TempDir("", "checkup") + dir, err := os.MkdirTemp("", "checkup") if err != nil { t.Fatalf("Cannot create temporary directory: %v", err) } @@ -54,7 +53,7 @@ func TestStorage(t *testing.T) { // Make sure check file bytes are correct checkfile := filepath.Join(specimen.Dir, name) - b, err := ioutil.ReadFile(checkfile) + b, err := os.ReadFile(checkfile) if err != nil { t.Fatalf("Expected no error reading body, got: %v", err) } diff --git a/storage/github/github.go b/storage/github/github.go index 0fd5d5c..c1befb8 100644 --- a/storage/github/github.go +++ b/storage/github/github.go @@ -153,7 +153,7 @@ func (gh *Storage) writeFile(filename string, sha string, contents []byte) error var err error var writeFunc func(context.Context, string, string, string, *github.RepositoryContentFileOptions) (*github.RepositoryContentResponse, *github.Response, error) opts := &github.RepositoryContentFileOptions{ - Message: github.String(fmt.Sprintf("[checkup] store %s %s", gh.fullPathName(filename), gh.commitMessageSuffix())), + Message: new(fmt.Sprintf("[checkup] store %s %s", gh.fullPathName(filename), gh.commitMessageSuffix())), Content: contents, Committer: &github.CommitAuthor{ Name: &gh.CommitterName, @@ -171,7 +171,7 @@ func (gh *Storage) writeFile(filename string, sha string, contents []byte) error writeFunc = gh.client.Repositories.CreateFile log.Printf("github: creating %s on branch '%s'", gh.fullPathName(filename), gh.Branch) } else { - opts.SHA = github.String(sha) + opts.SHA = new(sha) writeFunc = gh.client.Repositories.UpdateFile log.Printf("github: updating %s on branch '%s'", gh.fullPathName(filename), gh.Branch) } @@ -205,8 +205,8 @@ func (gh *Storage) deleteFile(filename string, sha string) error { gh.RepositoryName, gh.fullPathName(filename), &github.RepositoryContentFileOptions{ - Message: github.String(fmt.Sprintf("[checkup] delete %s %s", gh.fullPathName(filename), gh.commitMessageSuffix())), - SHA: github.String(sha), + Message: new(fmt.Sprintf("[checkup] delete %s %s", gh.fullPathName(filename), gh.commitMessageSuffix())), + SHA: new(sha), Branch: &gh.Branch, Committer: &github.CommitAuthor{ Name: &gh.CommitterName, diff --git a/storage/github/github_test.go b/storage/github/github_test.go index 474bde4..2ac00ca 100644 --- a/storage/github/github_test.go +++ b/storage/github/github_test.go @@ -32,7 +32,7 @@ var ( resultsBytes = []byte(`[{"title":"Testing"}]`) ) -func mustWriteJSON(w io.Writer, data interface{}) { +func mustWriteJSON(w io.Writer, data any) { if err := json.NewEncoder(w).Encode(data); err != nil { panic(err) } @@ -42,7 +42,7 @@ func base64Encoded(input []byte) string { return base64.StdEncoding.EncodeToString(input) } -func toJSON(data interface{}) []byte { +func toJSON(data any) []byte { encoded, err := json.Marshal(data) if err != nil { panic(err) @@ -62,15 +62,15 @@ func pathForIndex(path string) string { return filepath.Base(path) } -func repositoryContent(path, serverSHAForRepo string, data interface{}) *github.RepositoryContent { +func repositoryContent(path, serverSHAForRepo string, data any) *github.RepositoryContent { return &github.RepositoryContent{ - Type: github.String("file"), - Encoding: github.String("base64"), - Size: github.Int(len(base64Encoded(toJSON(data)))), - Name: github.String(filepath.Base(path)), - Path: github.String(path), - Content: github.String(base64Encoded(toJSON(data))), - SHA: github.String(serverSHAForRepo), + Type: new("file"), + Encoding: new("base64"), + Size: new(len(base64Encoded(toJSON(data)))), + Name: new(filepath.Base(path)), + Path: new(path), + Content: new(base64Encoded(toJSON(data))), + SHA: new(serverSHAForRepo), } } @@ -116,10 +116,10 @@ func withGitHubServer(t *testing.T, specimen Storage, f func(*github.Client)) { t.Errorf("Request method: %v, want %v", got, want) } mustWriteJSON(w, github.Reference{ - Ref: github.String("refs/heads/" + specimen.Branch), + Ref: new("refs/heads/" + specimen.Branch), Object: &github.GitObject{ - Type: github.String("commit"), - SHA: github.String(serverSHAForRepo), + Type: new("commit"), + SHA: new(serverSHAForRepo), }, }) }) @@ -136,21 +136,21 @@ func withGitHubServer(t *testing.T, specimen Storage, f func(*github.Client)) { entries := []github.TreeEntry{ { - SHA: github.String(sha(toJSON(index))), - Path: github.String(filepath.Join(specimen.Dir, "index.json")), - Content: github.String(base64Encoded(toJSON(index))), + SHA: new(sha(toJSON(index))), + Path: new(filepath.Join(specimen.Dir, "index.json")), + Content: new(base64Encoded(toJSON(index))), }, } for filename := range index { entries = append(entries, github.TreeEntry{ - SHA: github.String(sha(resultsBytes)), - Path: github.String(filepath.Join(specimen.Dir, filename)), - Content: github.String(string(resultsBytes)), + SHA: new(sha(resultsBytes)), + Path: new(filepath.Join(specimen.Dir, filename)), + Content: new(string(resultsBytes)), }) } mustWriteJSON(w, github.Tree{ - SHA: github.String(serverSHAForRepo), + SHA: new(serverSHAForRepo), Entries: entries, }) }) @@ -196,7 +196,7 @@ func withGitHubServer(t *testing.T, specimen Storage, f func(*github.Client)) { serverSHAForRepo = sha(toJSON(gitRepo)) mustWriteJSON(w, github.RepositoryContentResponse{ Content: repositoryContent(path, serverSHAForRepo, index), - Commit: github.Commit{SHA: github.String(serverSHAForRepo)}, + Commit: github.Commit{SHA: new(serverSHAForRepo)}, }) return } @@ -243,7 +243,7 @@ func withGitHubServer(t *testing.T, specimen Storage, f func(*github.Client)) { mustWriteJSON(w, github.RepositoryContentResponse{ Content: repositoryContent(path, serverSHAForRepo, results), Commit: github.Commit{ - SHA: github.String(serverSHAForRepo), + SHA: new(serverSHAForRepo), }, }) return @@ -290,7 +290,7 @@ func withGitHubServer(t *testing.T, specimen Storage, f func(*github.Client)) { // Response is the same if updating or creating. mustWriteJSON(w, github.RepositoryContentResponse{ Commit: github.Commit{ - SHA: github.String(serverSHAForRepo), + SHA: new(serverSHAForRepo), }, }) return diff --git a/storage/s3/s3.go b/storage/s3/s3.go index b46abba..d1c4745 100644 --- a/storage/s3/s3.go +++ b/storage/s3/s3.go @@ -113,7 +113,7 @@ func (s Storage) Maintain() error { Bucket: &s.Bucket, Delete: &s3.Delete{ Objects: objsToDelete, - Quiet: aws.Bool(true), + Quiet: new(true), }, } @@ -168,7 +168,7 @@ func (s Storage) Provision() (types.ProvisionInfo, error) { // Restrict the user to only reading S3 buckets _, err = svcIam.AttachUserPolicy(&iam.AttachUserPolicyInput{ - PolicyArn: aws.String("arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"), + PolicyArn: new("arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"), UserName: aws.String(iamUser), }) if err != nil { @@ -205,10 +205,10 @@ func (s Storage) Provision() (types.ProvisionInfo, error) { CORSConfiguration: &s3.CORSConfiguration{ CORSRules: []*s3.CORSRule{ { - AllowedOrigins: []*string{aws.String("*")}, - AllowedMethods: []*string{aws.String("GET"), aws.String("HEAD")}, - ExposeHeaders: []*string{aws.String("ETag")}, - AllowedHeaders: []*string{aws.String("*")}, + AllowedOrigins: []*string{new("*")}, + AllowedMethods: []*string{new("GET"), new("HEAD")}, + ExposeHeaders: []*string{new("ETag")}, + AllowedHeaders: []*string{new("*")}, MaxAgeSeconds: aws.Int64(3000), }, }, @@ -221,7 +221,7 @@ func (s Storage) Provision() (types.ProvisionInfo, error) { // Set its policy to allow getting objects _, err = svcS3.PutBucketPolicy(&s3.PutBucketPolicyInput{ Bucket: &s.Bucket, - Policy: aws.String(`{ + Policy: new(`{ "Version": "2012-10-17", "Statement": [ { diff --git a/storage/s3/s3_test.go b/storage/s3/s3_test.go index 01633a3..2b7614a 100644 --- a/storage/s3/s3_test.go +++ b/storage/s3/s3_test.go @@ -2,7 +2,8 @@ package s3 import ( "bytes" - "io/ioutil" + "io" + "strconv" "strings" "testing" @@ -73,7 +74,7 @@ func TestS3Store(t *testing.T) { } // Make sure body bytes are correct - bodyBytes, err := ioutil.ReadAll(fakes3.input.Body) + bodyBytes, err := io.ReadAll(fakes3.input.Body) if err != nil { t.Fatalf("Expected no error reading body, got: %v", err) } @@ -122,11 +123,11 @@ func (s *s3Mock) ListObjects(input *s3.ListObjectsInput) (*s3.ListObjectsOutput, return &s3.ListObjectsOutput{ Contents: []*s3.Object{ { - Key: aws.String("foobar"), + Key: new("foobar"), LastModified: new(time.Time), }, }, - IsTruncated: aws.Bool(input.Marker == nil), + IsTruncated: new(input.Marker == nil), }, nil }