-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
V3: Add initial version of new OpenAPI generator (#602)
# Description Aim of this change is to directly generate the new go bindings from the OpenAPI spec with our homemade generator. This let us able to solve all the blockers we have with the current version and the https://github.com/deepmap/oapi-codegen generator. The Goal of this change, is to generate code close as possible to the Spec, with a ready to use, nice user experience. --------- Signed-off-by: Pierre-Emmanuel Jacquier <[email protected]>
- Loading branch information
1 parent
edf05b9
commit 91fb6d3
Showing
382 changed files
with
81,254 additions
and
2 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
Submodule go.mk
updated
7 files
+6 −0 | .github/actions/release/action.yaml | |
+3 −0 | .gitignore | |
+6 −1 | release.mk | |
+46 −0 | scripts/aptly.conf.template | |
+114 −0 | scripts/publish-deb-artifact-to-sos.sh | |
+68 −0 | scripts/publish-rpm-artifact-to-sos.sh | |
+3 −0 | version.mk |
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 |
---|---|---|
@@ -1,20 +1,34 @@ | ||
module github.com/exoscale/egoscale | ||
|
||
require ( | ||
github.com/BluntSporks/abbreviation v0.0.0-20150522120346-096cdb48bafa | ||
github.com/deepmap/oapi-codegen v1.9.1 | ||
github.com/go-playground/validator/v10 v10.9.0 | ||
github.com/gofrs/uuid v4.4.0+incompatible | ||
github.com/google/uuid v1.3.1 | ||
github.com/hashicorp/go-retryablehttp v0.7.1 | ||
github.com/pb33f/libopenapi v0.11.0 | ||
github.com/stretchr/testify v1.8.2 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect | ||
github.com/go-playground/locales v0.14.0 // indirect | ||
github.com/go-playground/universal-translator v0.18.0 // indirect | ||
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect | ||
github.com/leodido/go-urn v1.2.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
github.com/stretchr/objx v0.5.0 // indirect | ||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect | ||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect | ||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect | ||
golang.org/x/sync v0.1.0 // indirect | ||
golang.org/x/sys v0.1.0 // indirect | ||
golang.org/x/text v0.3.7 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) | ||
|
||
go 1.17 | ||
go 1.20 | ||
|
||
retract v1.19.0 // Published accidentally. |
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,12 @@ | ||
# Egoscale v3 | ||
|
||
**Egoscale v3** is based on a generator written from scratch with [libopenapi](https://github.com/pb33f/libopenapi). | ||
|
||
The core base of the generator is using libopenapi to parse and read the [Exoscale OpenAPI spec](https://openapi-v2.exoscale.com/source.yaml) and then generate the code from it. | ||
|
||
## Generate Egoscale v3 | ||
|
||
From the root repo | ||
```Bash | ||
make generate | ||
``` |
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,17 @@ | ||
package api | ||
|
||
import "errors" | ||
|
||
var ( | ||
// ErrNotFound represents an error indicating a non-existent resource. | ||
ErrNotFound = errors.New("resource not found") | ||
|
||
// ErrTooManyFound represents an error indicating multiple results found for a single resource. | ||
ErrTooManyFound = errors.New("multiple resources found") | ||
|
||
// ErrInvalidRequest represents an error indicating that the caller's request is invalid. | ||
ErrInvalidRequest = errors.New("invalid request") | ||
|
||
// ErrAPIError represents an error indicating an API-side issue. | ||
ErrAPIError = errors.New("API error") | ||
) |
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,96 @@ | ||
package api | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/http/httputil" | ||
"os" | ||
) | ||
|
||
// ErrorHandlerMiddleware is an Exoscale API HTTP client middleware that | ||
// returns concrete Go errors according to API response errors. | ||
type ErrorHandlerMiddleware struct { | ||
next http.RoundTripper | ||
} | ||
|
||
func NewAPIErrorHandlerMiddleware(next http.RoundTripper) *ErrorHandlerMiddleware { | ||
if next == nil { | ||
next = http.DefaultTransport | ||
} | ||
|
||
return &ErrorHandlerMiddleware{next: next} | ||
} | ||
|
||
func (m *ErrorHandlerMiddleware) RoundTrip(req *http.Request) (*http.Response, error) { | ||
resp, err := m.next.RoundTrip(req) | ||
if err != nil { | ||
// If the request returned a Go error don't bother analyzing the response | ||
// body, as there probably won't be any (e.g. connection timeout/refused). | ||
return resp, err | ||
} | ||
|
||
if resp.StatusCode >= 400 && resp.StatusCode <= 599 { | ||
var res struct { | ||
Message string `json:"message"` | ||
} | ||
|
||
data, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("error reading response body: %s", err) | ||
} | ||
|
||
if json.Valid(data) { | ||
if err = json.Unmarshal(data, &res); err != nil { | ||
return nil, fmt.Errorf("error unmarshaling response: %s", err) | ||
} | ||
} else { | ||
res.Message = string(data) | ||
} | ||
|
||
switch { | ||
case resp.StatusCode == http.StatusNotFound: | ||
return nil, ErrNotFound | ||
|
||
case resp.StatusCode >= 400 && resp.StatusCode < 500: | ||
return nil, fmt.Errorf("%w: %s", ErrInvalidRequest, res.Message) | ||
|
||
case resp.StatusCode >= 500: | ||
return nil, fmt.Errorf("%w: %s", ErrAPIError, res.Message) | ||
} | ||
} | ||
|
||
return resp, err | ||
} | ||
|
||
// TraceMiddleware is a client HTTP middleware that dumps HTTP requests and responses content. | ||
type TraceMiddleware struct { | ||
next http.RoundTripper | ||
} | ||
|
||
func NewTraceMiddleware(next http.RoundTripper) *TraceMiddleware { | ||
if next == nil { | ||
next = http.DefaultTransport | ||
} | ||
|
||
return &TraceMiddleware{next: next} | ||
} | ||
|
||
func (t *TraceMiddleware) RoundTrip(req *http.Request) (*http.Response, error) { | ||
if dump, err := httputil.DumpRequest(req, true); err == nil { | ||
fmt.Fprintf(os.Stderr, ">>> %s\n", dump) | ||
} | ||
|
||
fmt.Fprintln(os.Stderr, "----------------------------------------------------------------------") | ||
|
||
resp, err := t.next.RoundTrip(req) | ||
|
||
if resp != nil { | ||
if dump, err := httputil.DumpResponse(resp, true); err == nil { | ||
fmt.Fprintf(os.Stderr, "<<< %s\n", dump) | ||
} | ||
} | ||
|
||
return resp, err | ||
} |
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,75 @@ | ||
package api | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type testHandler struct { | ||
resStatus int | ||
resText string | ||
} | ||
|
||
func (h *testHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) { | ||
w.WriteHeader(h.resStatus) | ||
_, _ = w.Write([]byte(h.resText)) | ||
} | ||
|
||
func TestErrorHandlerMiddleware_RoundTrip(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
handler *testHandler | ||
testFunc func(t *testing.T, res *http.Response, err error) | ||
}{ | ||
{ | ||
name: "ErrNotFound", | ||
handler: &testHandler{resStatus: http.StatusNotFound}, | ||
testFunc: func(t *testing.T, res *http.Response, err error) { | ||
require.ErrorIs(t, err, ErrNotFound) | ||
require.Nil(t, res) | ||
}, | ||
}, | ||
{ | ||
name: "ErrInvalidRequest", | ||
handler: &testHandler{resStatus: http.StatusBadRequest}, | ||
testFunc: func(t *testing.T, res *http.Response, err error) { | ||
require.ErrorIs(t, err, ErrInvalidRequest) | ||
require.Nil(t, res) | ||
}, | ||
}, | ||
{ | ||
name: "ErrAPIError", | ||
handler: &testHandler{resStatus: http.StatusInternalServerError}, | ||
testFunc: func(t *testing.T, res *http.Response, err error) { | ||
require.ErrorIs(t, err, ErrAPIError) | ||
require.Nil(t, res) | ||
}, | ||
}, | ||
{ | ||
name: "OK", | ||
handler: &testHandler{resStatus: http.StatusOK, resText: "test"}, | ||
testFunc: func(t *testing.T, res *http.Response, err error) { | ||
require.NoError(t, err) | ||
actual, _ := io.ReadAll(res.Body) | ||
require.Equal(t, []byte("test"), actual) | ||
}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
testServer := httptest.NewServer(test.handler) | ||
defer testServer.Close() | ||
|
||
testClient := testServer.Client() | ||
testClient.Transport = &ErrorHandlerMiddleware{next: testClient.Transport} | ||
|
||
res, err := testClient.Get(testServer.URL) | ||
test.testFunc(t, res, err) | ||
}) | ||
} | ||
} |
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,127 @@ | ||
package api | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"sort" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// SecurityProviderExoscale represents an Exoscale public API security | ||
// provider. | ||
type SecurityProviderExoscale struct { | ||
// ReqExpire represents the request expiration duration. | ||
ReqExpire time.Duration | ||
|
||
apiKey string | ||
apiSecret string | ||
} | ||
|
||
// NewSecurityProvider returns a new Exoscale public API security | ||
// provider to sign API requests using the specified API key/secret. | ||
func NewSecurityProvider(apiKey, apiSecret string) (*SecurityProviderExoscale, error) { | ||
if apiKey == "" { | ||
return nil, errors.New("missing API key") | ||
} | ||
|
||
if apiSecret == "" { | ||
return nil, errors.New("missing API secret") | ||
} | ||
|
||
return &SecurityProviderExoscale{ | ||
ReqExpire: 10 * time.Minute, | ||
apiKey: apiKey, | ||
apiSecret: apiSecret, | ||
}, nil | ||
} | ||
|
||
// Intercept is an HTTP middleware that intercepts and signs client requests | ||
// before sending them to the API endpoint. | ||
func (s *SecurityProviderExoscale) Intercept(_ context.Context, req *http.Request) error { | ||
return s.signRequest(req, time.Now().UTC().Add(s.ReqExpire)) | ||
} | ||
|
||
func (s *SecurityProviderExoscale) signRequest(req *http.Request, expiration time.Time) error { | ||
var ( | ||
sigParts []string | ||
headerParts []string | ||
) | ||
|
||
// Request method/URL path | ||
sigParts = append(sigParts, fmt.Sprintf("%s %s", req.Method, req.URL.EscapedPath())) | ||
headerParts = append(headerParts, "EXO2-HMAC-SHA256 credential="+s.apiKey) | ||
|
||
// Request body if present | ||
body := "" | ||
if req.Body != nil { | ||
data, err := io.ReadAll(req.Body) | ||
if err != nil { | ||
return err | ||
} | ||
err = req.Body.Close() | ||
if err != nil { | ||
return err | ||
} | ||
body = string(data) | ||
req.Body = io.NopCloser(bytes.NewReader(data)) | ||
} | ||
sigParts = append(sigParts, body) | ||
|
||
// Request query string parameters | ||
// Important: this is order-sensitive, we have to have to sort parameters alphabetically to ensure signed | ||
// values match the names listed in the "signed-query-args=" signature pragma. | ||
signedParams, paramsValues := extractRequestParameters(req) | ||
sigParts = append(sigParts, paramsValues) | ||
if len(signedParams) > 0 { | ||
headerParts = append(headerParts, "signed-query-args="+strings.Join(signedParams, ";")) | ||
} | ||
|
||
// Request headers -- none at the moment | ||
// Note: the same order-sensitive caution for query string parameters applies to headers. | ||
sigParts = append(sigParts, "") | ||
|
||
// Request expiration date (UNIX timestamp, no line return) | ||
sigParts = append(sigParts, fmt.Sprint(expiration.Unix())) | ||
headerParts = append(headerParts, "expires="+fmt.Sprint(expiration.Unix())) | ||
|
||
h := hmac.New(sha256.New, []byte(s.apiSecret)) | ||
if _, err := h.Write([]byte(strings.Join(sigParts, "\n"))); err != nil { | ||
return err | ||
} | ||
headerParts = append(headerParts, "signature="+base64.StdEncoding.EncodeToString(h.Sum(nil))) | ||
|
||
req.Header.Set("Authorization", strings.Join(headerParts, ",")) | ||
|
||
return nil | ||
} | ||
|
||
// extractRequestParameters returns the list of request URL parameters names | ||
// and a strings concatenating the values of the parameters. | ||
func extractRequestParameters(req *http.Request) ([]string, string) { | ||
var ( | ||
names []string | ||
values string | ||
) | ||
|
||
for param, values := range req.URL.Query() { | ||
// Keep only parameters that hold exactly 1 value (i.e. no empty or multi-valued parameters) | ||
if len(values) == 1 { | ||
names = append(names, param) | ||
} | ||
} | ||
sort.Strings(names) | ||
|
||
for _, param := range names { | ||
values += req.URL.Query().Get(param) | ||
} | ||
|
||
return names, values | ||
} |
Oops, something went wrong.