-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from grafana/http-service
Implement build service
- Loading branch information
Showing
20 changed files
with
1,963 additions
and
321 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.