Skip to content

Commit

Permalink
Merge pull request #1 from grafana/http-service
Browse files Browse the repository at this point in the history
Implement build service
  • Loading branch information
pablochacin authored Jun 23, 2024
2 parents 8eaba6b + 6ecf25f commit 834994f
Show file tree
Hide file tree
Showing 20 changed files with 1,963 additions and 321 deletions.
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ issues:
- gocognit
- funlen
- lll
- gosec
- noctx
- path: js\/modules\/k6\/http\/.*_test\.go
linters:
# k6/http module's tests are quite complex because they often have several nested levels.
Expand Down Expand Up @@ -62,7 +64,6 @@ linters-settings:
- '^(fmt\\.Print(|f|ln)|print|println)$'
# Forbid everything in syscall except the uppercase constants
- '^syscall\.[^A-Z_]+$(# Using anything except constants from the syscall package is forbidden )?'
- '^logrus\.Logger$'

linters:
disable-all: true
Expand Down
104 changes: 104 additions & 0 deletions apiserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package k6build

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/sirupsen/logrus"
)

// BuildRequest defines a request to the build service
type BuildRequest struct {
K6Constrains string `json:"k6:omitempty"`
Dependencies []Dependency `json:"dependencies,omitempty"`
Platform string `json:"platformomitempty"`
}

// String returns a text serialization of the BuildRequest
func (r BuildRequest) String() string {
buffer := &bytes.Buffer{}
buffer.WriteString(fmt.Sprintf("platform: %s", r.Platform))
buffer.WriteString(fmt.Sprintf("k6: %s", r.K6Constrains))
for _, d := range r.Dependencies {
buffer.WriteString(fmt.Sprintf("%s:%q", d.Name, d.Constraints))
}
return buffer.String()
}

// BuildResponse defines the response for a BuildRequest
type BuildResponse struct {
Error string `json:"error:omitempty"`
Artifact Artifact `json:"artifact:omitempty"`
}

// APIServerConfig defines the configuration for the APIServer
type APIServerConfig struct {
BuildService BuildService
Log *logrus.Logger
}

// APIServer defines a k6build API server
type APIServer struct {
srv BuildService
log *logrus.Logger
}

// NewAPIServer creates a new build service API server
// TODO: add logger
func NewAPIServer(config APIServerConfig) *APIServer {
log := config.Log
if log == nil {
log = &logrus.Logger{Out: io.Discard}
}
return &APIServer{
srv: config.BuildService,
log: log,
}
}

// ServeHTTP implements the request handler for the build API server
func (a *APIServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
resp := BuildResponse{}

w.Header().Add("Content-Type", "application/json")

// ensure errors are reported and logged
defer func() {
if resp.Error != "" {
a.log.Error(resp.Error)
_ = json.NewEncoder(w).Encode(resp) //nolint:errchkjson
}
}()

req := BuildRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
resp.Error = fmt.Sprintf("invalid request: %s", err.Error())
return
}

a.log.Debugf("processing request %s", req.String())

artifact, err := a.srv.Build( //nolint:contextcheck
context.Background(),
req.Platform,
req.K6Constrains,
req.Dependencies,
)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
resp.Error = fmt.Sprintf("building artifact: %s", err.Error())
return
}

a.log.Debugf("returning artifact %s", artifact.String())

resp.Artifact = artifact
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(resp) //nolint:errchkjson
}
95 changes: 95 additions & 0 deletions apiserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package k6build

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

func TestAPIServer(t *testing.T) {
t.Parallel()

testCases := []struct {
title string
req BuildRequest
expect BuildResponse
}{
{
title: "build k6 v0.1.0 ",
req: BuildRequest{
Platform: "linux/amd64",
K6Constrains: "v0.1.0",
Dependencies: []Dependency{},
},
expect: BuildResponse{
Artifact: Artifact{
Dependencies: map[string]string{"k6": "v0.1.0"},
},
},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()

buildsrv, err := SetupTestLocalBuildService(
LocalBuildServiceConfig{
CacheDir: t.TempDir(),
Catalog: "testdata/catalog.json",
},
)
if err != nil {
t.Fatalf("test setup %v", err)
}

config := APIServerConfig{
BuildService: buildsrv,
}
apiserver := httptest.NewServer(NewAPIServer(config))

req := bytes.Buffer{}
err = json.NewEncoder(&req).Encode(&tc.req)
if err != nil {
t.Fatalf("test setup %v", err)
}

resp, err := http.Post(apiserver.URL, "application/json", &req)
if err != nil {
t.Fatalf("making request %v", err)
}
defer func() {
_ = resp.Body.Close()
}()

buildResponse := BuildResponse{}
err = json.NewDecoder(resp.Body).Decode(&buildResponse)
if err != nil {
t.Fatalf("decoding response %v", err)
}

if buildResponse.Error != tc.expect.Error {
t.Fatalf("expected error: %s got %s", tc.expect.Error, buildResponse.Error)
}

// don't check artifact if error is expected
if tc.expect.Error != "" {
return
}

diff := cmp.Diff(
tc.expect.Artifact.Dependencies,
buildResponse.Artifact.Dependencies,
cmpopts.SortSlices(dependencyComp))
if diff != "" {
t.Fatalf("dependencies don't match: %s\n", diff)
}
})
}
}
82 changes: 82 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Package k6build defines a service for building k8 binaries
package k6build

import (
"bytes"
"context"
"fmt"

"github.com/grafana/k6catalog"
"github.com/grafana/k6foundry"
)

const (
k6Dep = "k6"
)

// Dependency contains the properties of a k6 dependency.
type Dependency struct {
// Name is the name of the dependency.
Name string `json:"name,omitempty"`
// Constraints contains the version constraints of the dependency.
Constraints string `json:"constraints,omitempty"`
}

// Module defines an artifact dependency
type Module struct {
Path string `json:"path,omitempty"`
Version string `json:"vesion,omitempty"`
}

// Artifact defines a binary that can be downloaded
// TODO: add metadata (e.g. list of dependencies, checksum, date compiled)
type Artifact struct {
ID string `json:"id,omitempty"`
// URL to fetch the artifact's binary
URL string `json:"url,omitempty"`
// list of dependencies
Dependencies map[string]string `json:"dependencies,omitempty"`
// platform
Platform string `json:"platform,omitempty"`
// binary checksum (sha256)
Checksum string `json:"checksum,omitempty"`
}

// String returns a text serialization of the Artifact
func (a Artifact) String() string {
buffer := &bytes.Buffer{}
buffer.WriteString(fmt.Sprintf(" id: %s", a.ID))
buffer.WriteString(fmt.Sprintf("platform: %s", a.Platform))
for dep, version := range a.Dependencies {
buffer.WriteString(fmt.Sprintf(" %s:%q", dep, version))
}
buffer.WriteString(fmt.Sprintf(" checksum: %s", a.Checksum))
buffer.WriteString(fmt.Sprintf(" url: %s", a.URL))
return buffer.String()
}

// BuildService defines the interface of a build service
type BuildService interface {
// Build returns a k6 Artifact given its dependencies and version constrain
Build(ctx context.Context, platform string, k6Constrains string, deps []Dependency) (Artifact, error)
}

// implements the BuildService interface
type localBuildSrv struct {
catalog k6catalog.Catalog
builder k6foundry.Builder
cache Cache
}

// NewBuildService creates a build service
func NewBuildService(
catalog k6catalog.Catalog,
builder k6foundry.Builder,
cache Cache,
) BuildService {
return &localBuildSrv{
catalog: catalog,
builder: builder,
cache: cache,
}
}
Loading

0 comments on commit 834994f

Please sign in to comment.