-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Justin <[email protected]>
- Loading branch information
Showing
18 changed files
with
1,374 additions
and
109 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package distribution | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/containerd/containerd/namespaces" | ||
"github.com/containerd/nerdctl/pkg/config" | ||
dockertypes "github.com/docker/cli/cli/config/types" | ||
registrytypes "github.com/docker/docker/api/types/registry" | ||
"github.com/gorilla/mux" | ||
"github.com/runfinch/finch-daemon/api/auth" | ||
"github.com/runfinch/finch-daemon/api/response" | ||
"github.com/runfinch/finch-daemon/api/types" | ||
"github.com/runfinch/finch-daemon/pkg/errdefs" | ||
"github.com/runfinch/finch-daemon/pkg/flog" | ||
) | ||
|
||
//go:generate mockgen --destination=../../../mocks/mocks_distribution/distributionsvc.go -package=mocks_distribution github.com/runfinch/finch-daemon/api/handlers/distribution Service | ||
type Service interface { | ||
Inspect(ctx context.Context, name string, authCfg *dockertypes.AuthConfig) (*registrytypes.DistributionInspect, error) | ||
} | ||
|
||
func RegisterHandlers(r types.VersionedRouter, service Service, conf *config.Config, logger flog.Logger) { | ||
h := newHandler(service, conf, logger) | ||
r.HandleFunc("/distribution/{name:.*}/json", h.inspect, http.MethodGet) | ||
} | ||
|
||
func newHandler(service Service, conf *config.Config, logger flog.Logger) *handler { | ||
return &handler{ | ||
service: service, | ||
Config: conf, | ||
logger: logger, | ||
} | ||
} | ||
|
||
type handler struct { | ||
service Service | ||
Config *config.Config | ||
logger flog.Logger | ||
} | ||
|
||
func (h *handler) inspect(w http.ResponseWriter, r *http.Request) { | ||
name := mux.Vars(r)["name"] | ||
// get auth creds from header | ||
authCfg, err := auth.DecodeAuthConfig(r.Header.Get(auth.AuthHeader)) | ||
if err != nil { | ||
response.SendErrorResponse(w, http.StatusBadRequest, fmt.Errorf("failed to decode auth header: %s", err)) | ||
return | ||
} | ||
ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace) | ||
inspectRes, err := h.service.Inspect(ctx, name, authCfg) | ||
// map the error into http status code and send response. | ||
if err != nil { | ||
var code int | ||
// according to the docs https://docs.docker.com/reference/api/engine/version/v1.47/#tag/Distribution/operation/DistributionInspect | ||
// there are 3 possible error codes: 200, 401, 500 | ||
// in practice, it seems 403 is used rather than 401 and 400 is used for client input errors | ||
switch { | ||
case errdefs.IsInvalidFormat(err): | ||
code = http.StatusBadRequest | ||
case errdefs.IsUnauthenticated(err), errdefs.IsNotFound(err): | ||
code = http.StatusForbidden | ||
default: | ||
code = http.StatusInternalServerError | ||
} | ||
h.logger.Debugf("Inspect Distribution API failed. Status code %d, Message: %s", code, err) | ||
response.SendErrorResponse(w, code, err) | ||
return | ||
} | ||
|
||
// return JSON response | ||
response.JSON(w, http.StatusOK, inspectRes) | ||
} |
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,122 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package distribution | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/containerd/nerdctl/pkg/config" | ||
registrytypes "github.com/docker/docker/api/types/registry" | ||
"github.com/golang/mock/gomock" | ||
"github.com/gorilla/mux" | ||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
|
||
"github.com/runfinch/finch-daemon/mocks/mocks_distribution" | ||
"github.com/runfinch/finch-daemon/mocks/mocks_logger" | ||
"github.com/runfinch/finch-daemon/pkg/errdefs" | ||
) | ||
|
||
// TestDistributionHandler function is the entry point of distribution handler package's unit test using ginkgo. | ||
func TestDistributionHandler(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "UnitTests - Distribution APIs Handler") | ||
} | ||
|
||
var _ = Describe("Distribution Inspect API", func() { | ||
var ( | ||
mockCtrl *gomock.Controller | ||
logger *mocks_logger.Logger | ||
service *mocks_distribution.MockService | ||
h *handler | ||
rr *httptest.ResponseRecorder | ||
name string | ||
req *http.Request | ||
ociPlatformAmd ocispec.Platform | ||
ociPlatformArm ocispec.Platform | ||
resp registrytypes.DistributionInspect | ||
respJSON []byte | ||
) | ||
BeforeEach(func() { | ||
mockCtrl = gomock.NewController(GinkgoT()) | ||
defer mockCtrl.Finish() | ||
logger = mocks_logger.NewLogger(mockCtrl) | ||
service = mocks_distribution.NewMockService(mockCtrl) | ||
c := config.Config{} | ||
h = newHandler(service, &c, logger) | ||
rr = httptest.NewRecorder() | ||
name = "test-image" | ||
var err error | ||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/distribution/%s/json", name), nil) | ||
Expect(err).Should(BeNil()) | ||
req = mux.SetURLVars(req, map[string]string{"name": name}) | ||
ociPlatformAmd = ocispec.Platform{ | ||
Architecture: "amd64", | ||
OS: "linux", | ||
} | ||
ociPlatformArm = ocispec.Platform{ | ||
Architecture: "amd64", | ||
OS: "linux", | ||
} | ||
resp = registrytypes.DistributionInspect{ | ||
Descriptor: ocispec.Descriptor{ | ||
MediaType: ocispec.MediaTypeImageManifest, | ||
Digest: "sha256:9bae60c369e612488c2a089c38737277a4823a3af97ec6866c3b4ad05251bfa5", | ||
Size: 2, | ||
URLs: []string{}, | ||
Annotations: map[string]string{}, | ||
Data: []byte{}, | ||
Platform: &ociPlatformAmd, | ||
}, | ||
Platforms: []ocispec.Platform{ | ||
ociPlatformAmd, | ||
ociPlatformArm, | ||
}, | ||
} | ||
respJSON, err = json.Marshal(resp) | ||
Expect(err).Should(BeNil()) | ||
}) | ||
Context("handler", func() { | ||
It("should return inspect object and 200 status code upon success", func() { | ||
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(&resp, nil) | ||
|
||
// handler should return response object with 200 status code | ||
h.inspect(rr, req) | ||
Expect(rr.Body).Should(MatchJSON(respJSON)) | ||
Expect(rr).Should(HaveHTTPStatus(http.StatusOK)) | ||
}) | ||
It("should return 403 status code if image resolution fails due to lack of credentials", func() { | ||
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(nil, errdefs.NewForbidden(fmt.Errorf("access denied"))) | ||
logger.EXPECT().Debugf(gomock.Any(), gomock.Any()) | ||
|
||
// handler should return error message with 404 status code | ||
h.inspect(rr, req) | ||
Expect(rr.Body).Should(MatchJSON(`{"message": "access denied"}`)) | ||
Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden)) | ||
}) | ||
It("should return 404 status code if image was not found", func() { | ||
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(nil, errdefs.NewNotFound(fmt.Errorf("no such image"))) | ||
logger.EXPECT().Debugf(gomock.Any(), gomock.Any()) | ||
|
||
// handler should return error message with 404 status code | ||
h.inspect(rr, req) | ||
Expect(rr.Body).Should(MatchJSON(`{"message": "no such image"}`)) | ||
Expect(rr).Should(HaveHTTPStatus(http.StatusNotFound)) | ||
}) | ||
It("should return 500 status code if service returns an error message", func() { | ||
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(nil, fmt.Errorf("error")) | ||
logger.EXPECT().Debugf(gomock.Any(), gomock.Any()) | ||
|
||
// handler should return error message | ||
h.inspect(rr, req) | ||
Expect(rr.Body).Should(MatchJSON(`{"message": "error"}`)) | ||
Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError)) | ||
}) | ||
}) | ||
}) |
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
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
Oops, something went wrong.