Skip to content

Commit

Permalink
Merge pull request #12 from puerco/client
Browse files Browse the repository at this point in the history
Add REST client
  • Loading branch information
JAORMX authored May 30, 2024
2 parents 599e269 + db0a359 commit 10d5797
Show file tree
Hide file tree
Showing 6 changed files with 492 additions and 3 deletions.
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ go 1.22.1
require (
github.com/BurntSushi/toml v1.4.0
github.com/google/go-github/v61 v61.0.0
github.com/stretchr/testify v1.9.0
golang.org/x/oauth2 v0.20.0
)

require github.com/google/go-querystring v1.1.0 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
151 changes: 151 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2024 Stacklok, 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 client provides a rest client to talk to the Trusty API.
package client

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"

"github.com/stacklok/trusty-sdk-go/pkg/types"
)

const (
defaultEndpoint = "https://api.trustypkg.dev"
endpointEnvVar = "TRUSTY_ENDPOINT"
reportPath = "v1/report"
)

// Options configures the Trusty API client
type Options struct {
HttpClient netClient
BaseURL string
}

// DefaultOptions is the default Trusty client options set
var DefaultOptions = Options{
HttpClient: &http.Client{},
BaseURL: defaultEndpoint,
}

type netClient interface {
Do(req *http.Request) (*http.Response, error)
}

// New returns a new Trusty REST client
func New() *Trusty {
opts := DefaultOptions
if ep := os.Getenv(endpointEnvVar); ep != "" {
opts.BaseURL = ep
}
return NewWithOptions(opts)
}

// NewWithOptions returns a new client with the dspecified options set
func NewWithOptions(opts Options) *Trusty {
return &Trusty{
Options: opts,
}
}

func urlFromEndpointAndPaths(
baseUrl, endpoint string, params map[string]string,
) (*url.URL, error) {
u, err := url.Parse(baseUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
}
u = u.JoinPath(endpoint)

// Add query parameters for package_name and package_type
q := u.Query()
for k, v := range params {
q.Set(k, v)
}
u.RawQuery = q.Encode()

return u, nil
}

// Trusty is the main trusty client
type Trusty struct {
Options Options
}

// newRequest buids a new http GET request using the preconfigured trusty API uri
func (t *Trusty) newRequest(ctx context.Context, path string, params map[string]string) (*http.Request, error) {
u, err := urlFromEndpointAndPaths(t.Options.BaseURL, path, params)
if err != nil {
return nil, fmt.Errorf("could not parse endpoint: %w", err)
}

req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("could not create request: %w", err)
}
req = req.WithContext(ctx)
return req, nil
}

// Report returns a dependency report with all the data that Trust has
// available for a package
func (t *Trusty) Report(ctx context.Context, dep *types.Dependency) (*types.Reply, error) {
// Check dependency:
errs := []error{}
if dep.Name == "" {
errs = append(errs, fmt.Errorf("dependency has no name defined"))
}
if dep.Ecosystem.AsString() == "" {
errs = append(errs, fmt.Errorf("dependency has no ecosystem set"))
}

preErr := errors.Join(errs...)
if preErr != nil {
return nil, preErr
}

params := map[string]string{
"package_name": dep.Name,
"package_type": strings.ToLower(dep.Ecosystem.AsString()),
}
req, err := t.newRequest(ctx, reportPath, params)
if err != nil {
return nil, fmt.Errorf("could not create request: %w", err)
}

resp, err := t.Options.HttpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("could not send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-200 response: %d", resp.StatusCode)
}

var r types.Reply
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&r); err != nil {
return nil, fmt.Errorf("could not unmarshal response: %w", err)
}

return &r, nil
}
192 changes: 192 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright 2024 Stacklok, 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 client provides a rest client to talk to the Trusty API.
package client

import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"

"github.com/stacklok/trusty-sdk-go/pkg/types"
"github.com/stretchr/testify/require"
)

// fakeClient mocks the http client used by the trusty client
type fakeClient struct {
resp *http.Response
err error
}

func (fc *fakeClient) Do(_ *http.Request) (*http.Response, error) {
return fc.resp, fc.err
}

func buildReader(s string) io.ReadCloser {
stringReader := strings.NewReader(s)
return io.NopCloser(stringReader)
}

func TestReport(t *testing.T) {
t.Parallel()
respBody := `{"package_name":"requestts","package_type":"pypi"}`

testdep := &types.Dependency{
Name: "requestts",
Ecosystem: 1,
}

for _, tc := range []struct {
name string
dep *types.Dependency
prepare func(*fakeClient)
expected *types.Reply
mustErr bool
}{
{
name: "normal",
dep: testdep,
prepare: func(fc *fakeClient) {
fc.resp = &http.Response{
StatusCode: http.StatusOK,
Body: buildReader(respBody),
}
},
expected: &types.Reply{
PackageName: "requestts",
PackageType: "pypi",
},
},
{
name: "no-dep-name",
dep: &types.Dependency{
Ecosystem: 1,
},
prepare: func(_ *fakeClient) {},
mustErr: true,
},
{
name: "no-dep-ecosystem",
dep: &types.Dependency{
Name: "test",
},
prepare: func(_ *fakeClient) {},
mustErr: true,
},
{
name: "http-fails",
dep: testdep,
prepare: func(fc *fakeClient) {
fc.err = fmt.Errorf("fake error")
},
mustErr: true,
},
{
name: "http-non-200",
dep: testdep,
prepare: func(fc *fakeClient) {
fc.resp = &http.Response{
Body: buildReader(respBody),
Status: "Not found",
StatusCode: 404,
}
},
mustErr: true,
},
{
name: "bad-response-json",
dep: testdep,
prepare: func(fc *fakeClient) {
fc.resp = &http.Response{
Body: buildReader("HEy Fr1end!"),
Status: "OK",
StatusCode: 200,
}
},
mustErr: true,
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fake := &fakeClient{}
tc.prepare(fake)
client := &Trusty{
Options: Options{
HttpClient: fake,
BaseURL: defaultEndpoint,
},
}

res, err := client.Report(context.Background(), tc.dep)
if tc.mustErr {
require.Error(t, err)
return
}

require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, tc.expected.PackageName, res.PackageName)
})
}
}

func TestUrlFromEndpointAndPaths(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
baseUrl string
endpoint string
params map[string]string
expected string
mustErr bool
}{
{
name: "no-query",
endpoint: "/test/",
baseUrl: defaultEndpoint,
expected: "https://api.trustypkg.dev/test/",
},
{
name: "query-string",
endpoint: "/test/",
baseUrl: defaultEndpoint,
params: map[string]string{"key": "value"},
expected: "https://api.trustypkg.dev/test/?key=value",
},
{
name: "invalid-base",
endpoint: "/test/",
baseUrl: "Even!\nFlow!",
mustErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
tc := tc
t.Parallel()
res, err := urlFromEndpointAndPaths(tc.baseUrl, tc.endpoint, tc.params)
if tc.mustErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expected, res.String())

})
}
}
Loading

0 comments on commit 10d5797

Please sign in to comment.