Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add distribution API (with bug fix) #121

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions api/handlers/distribution/distribution.go
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"]
coderbirju marked this conversation as resolved.
Show resolved Hide resolved
// 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)
}
122 changes: 122 additions & 0 deletions api/handlers/distribution/distribution_test.go
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.NewUnauthenticated(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 403 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.StatusForbidden))
})
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))
})
})
})
19 changes: 11 additions & 8 deletions api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/runfinch/finch-daemon/api/handlers/builder"
"github.com/runfinch/finch-daemon/api/handlers/container"
"github.com/runfinch/finch-daemon/api/handlers/distribution"
"github.com/runfinch/finch-daemon/api/handlers/exec"
"github.com/runfinch/finch-daemon/api/handlers/image"
"github.com/runfinch/finch-daemon/api/handlers/network"
Expand All @@ -31,14 +32,15 @@ import (

// Options defines the router options to be passed into the handlers.
type Options struct {
Config *config.Config
ContainerService container.Service
ImageService image.Service
NetworkService network.Service
SystemService system.Service
BuilderService builder.Service
VolumeService volume.Service
ExecService exec.Service
Config *config.Config
ContainerService container.Service
ImageService image.Service
NetworkService network.Service
SystemService system.Service
BuilderService builder.Service
VolumeService volume.Service
ExecService exec.Service
DistributionService distribution.Service

// NerdctlWrapper wraps the interactions with nerdctl to build
NerdctlWrapper *backend.NerdctlWrapper
Expand All @@ -59,6 +61,7 @@ func New(opts *Options) http.Handler {
builder.RegisterHandlers(vr, opts.BuilderService, opts.Config, logger, opts.NerdctlWrapper)
volume.RegisterHandlers(vr, opts.VolumeService, opts.Config, logger)
exec.RegisterHandlers(vr, opts.ExecService, opts.Config, logger)
distribution.RegisterHandlers(vr, opts.DistributionService, opts.Config, logger)
return ghandlers.LoggingHandler(os.Stderr, r)
}

Expand Down
20 changes: 11 additions & 9 deletions cmd/finch-daemon/router_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/runfinch/finch-daemon/internal/backend"
"github.com/runfinch/finch-daemon/internal/service/builder"
"github.com/runfinch/finch-daemon/internal/service/container"
"github.com/runfinch/finch-daemon/internal/service/distribution"
"github.com/runfinch/finch-daemon/internal/service/exec"
"github.com/runfinch/finch-daemon/internal/service/image"
"github.com/runfinch/finch-daemon/internal/service/network"
Expand Down Expand Up @@ -101,14 +102,15 @@ func createRouterOptions(
tarExtractor := archive.NewTarExtractor(ecc.NewExecCmdCreator(), logger)

return &router.Options{
Config: conf,
ContainerService: container.NewService(clientWrapper, ncWrapper, logger, fs, tarCreator, tarExtractor),
ImageService: image.NewService(clientWrapper, ncWrapper, logger),
NetworkService: network.NewService(clientWrapper, ncWrapper, logger),
SystemService: system.NewService(clientWrapper, ncWrapper, logger),
BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor),
VolumeService: volume.NewService(ncWrapper, logger),
ExecService: exec.NewService(clientWrapper, logger),
NerdctlWrapper: ncWrapper,
Config: conf,
ContainerService: container.NewService(clientWrapper, ncWrapper, logger, fs, tarCreator, tarExtractor),
ImageService: image.NewService(clientWrapper, ncWrapper, logger),
NetworkService: network.NewService(clientWrapper, ncWrapper, logger),
SystemService: system.NewService(clientWrapper, ncWrapper, logger),
BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor),
VolumeService: volume.NewService(ncWrapper, logger),
ExecService: exec.NewService(clientWrapper, logger),
DistributionService: distribution.NewService(clientWrapper, ncWrapper, logger),
NerdctlWrapper: ncWrapper,
}
}
3 changes: 3 additions & 0 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func TestRun(t *testing.T) {
// functional test for system api
tests.SystemVersion(opt)
tests.SystemEvents(opt)

// functional test for distribution api
tests.DistributionInspect(opt)
})

gomega.RegisterFailHandler(ginkgo.Fail)
Expand Down
Loading
Loading