Skip to content

Commit

Permalink
feat: add distribution API
Browse files Browse the repository at this point in the history
Signed-off-by: Justin <[email protected]>
  • Loading branch information
pendo324 committed Dec 17, 2024
1 parent 5c99f3e commit f43c524
Show file tree
Hide file tree
Showing 18 changed files with 1,374 additions and 109 deletions.
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"]
// 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.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))
})
})
})
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

0 comments on commit f43c524

Please sign in to comment.