Skip to content

Commit da0dab7

Browse files
authored
feat: add distribution API (with bug fix) (runfinch#121)
* feat: add distribution API --------- Signed-off-by: Justin <[email protected]> Signed-off-by: Justin Alvarez <[email protected]>
1 parent 9fda9cd commit da0dab7

File tree

18 files changed

+1368
-109
lines changed

18 files changed

+1368
-109
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package distribution
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
11+
"github.com/containerd/containerd/namespaces"
12+
"github.com/containerd/nerdctl/pkg/config"
13+
dockertypes "github.com/docker/cli/cli/config/types"
14+
registrytypes "github.com/docker/docker/api/types/registry"
15+
"github.com/gorilla/mux"
16+
"github.com/runfinch/finch-daemon/api/auth"
17+
"github.com/runfinch/finch-daemon/api/response"
18+
"github.com/runfinch/finch-daemon/api/types"
19+
"github.com/runfinch/finch-daemon/pkg/errdefs"
20+
"github.com/runfinch/finch-daemon/pkg/flog"
21+
)
22+
23+
//go:generate mockgen --destination=../../../mocks/mocks_distribution/distributionsvc.go -package=mocks_distribution github.com/runfinch/finch-daemon/api/handlers/distribution Service
24+
type Service interface {
25+
Inspect(ctx context.Context, name string, authCfg *dockertypes.AuthConfig) (*registrytypes.DistributionInspect, error)
26+
}
27+
28+
func RegisterHandlers(r types.VersionedRouter, service Service, conf *config.Config, logger flog.Logger) {
29+
h := newHandler(service, conf, logger)
30+
r.HandleFunc("/distribution/{name:.*}/json", h.inspect, http.MethodGet)
31+
}
32+
33+
func newHandler(service Service, conf *config.Config, logger flog.Logger) *handler {
34+
return &handler{
35+
service: service,
36+
Config: conf,
37+
logger: logger,
38+
}
39+
}
40+
41+
type handler struct {
42+
service Service
43+
Config *config.Config
44+
logger flog.Logger
45+
}
46+
47+
func (h *handler) inspect(w http.ResponseWriter, r *http.Request) {
48+
name := mux.Vars(r)["name"]
49+
// get auth creds from header
50+
authCfg, err := auth.DecodeAuthConfig(r.Header.Get(auth.AuthHeader))
51+
if err != nil {
52+
response.SendErrorResponse(w, http.StatusBadRequest, fmt.Errorf("failed to decode auth header: %s", err))
53+
return
54+
}
55+
ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)
56+
inspectRes, err := h.service.Inspect(ctx, name, authCfg)
57+
// map the error into http status code and send response.
58+
if err != nil {
59+
var code int
60+
// according to the docs https://docs.docker.com/reference/api/engine/version/v1.47/#tag/Distribution/operation/DistributionInspect
61+
// there are 3 possible error codes: 200, 401, 500
62+
// in practice, it seems 403 is used rather than 401 and 400 is used for client input errors
63+
switch {
64+
case errdefs.IsInvalidFormat(err):
65+
code = http.StatusBadRequest
66+
case errdefs.IsUnauthenticated(err), errdefs.IsNotFound(err):
67+
code = http.StatusForbidden
68+
default:
69+
code = http.StatusInternalServerError
70+
}
71+
h.logger.Debugf("Inspect Distribution API failed. Status code %d, Message: %s", code, err)
72+
response.SendErrorResponse(w, code, err)
73+
return
74+
}
75+
76+
// return JSON response
77+
response.JSON(w, http.StatusOK, inspectRes)
78+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package distribution
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
13+
"github.com/containerd/nerdctl/pkg/config"
14+
registrytypes "github.com/docker/docker/api/types/registry"
15+
"github.com/golang/mock/gomock"
16+
"github.com/gorilla/mux"
17+
. "github.com/onsi/ginkgo/v2"
18+
. "github.com/onsi/gomega"
19+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
20+
21+
"github.com/runfinch/finch-daemon/mocks/mocks_distribution"
22+
"github.com/runfinch/finch-daemon/mocks/mocks_logger"
23+
"github.com/runfinch/finch-daemon/pkg/errdefs"
24+
)
25+
26+
// TestDistributionHandler function is the entry point of distribution handler package's unit test using ginkgo.
27+
func TestDistributionHandler(t *testing.T) {
28+
RegisterFailHandler(Fail)
29+
RunSpecs(t, "UnitTests - Distribution APIs Handler")
30+
}
31+
32+
var _ = Describe("Distribution Inspect API", func() {
33+
var (
34+
mockCtrl *gomock.Controller
35+
logger *mocks_logger.Logger
36+
service *mocks_distribution.MockService
37+
h *handler
38+
rr *httptest.ResponseRecorder
39+
name string
40+
req *http.Request
41+
ociPlatformAmd ocispec.Platform
42+
ociPlatformArm ocispec.Platform
43+
resp registrytypes.DistributionInspect
44+
respJSON []byte
45+
)
46+
BeforeEach(func() {
47+
mockCtrl = gomock.NewController(GinkgoT())
48+
defer mockCtrl.Finish()
49+
logger = mocks_logger.NewLogger(mockCtrl)
50+
service = mocks_distribution.NewMockService(mockCtrl)
51+
c := config.Config{}
52+
h = newHandler(service, &c, logger)
53+
rr = httptest.NewRecorder()
54+
name = "test-image"
55+
var err error
56+
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/distribution/%s/json", name), nil)
57+
Expect(err).Should(BeNil())
58+
req = mux.SetURLVars(req, map[string]string{"name": name})
59+
ociPlatformAmd = ocispec.Platform{
60+
Architecture: "amd64",
61+
OS: "linux",
62+
}
63+
ociPlatformArm = ocispec.Platform{
64+
Architecture: "amd64",
65+
OS: "linux",
66+
}
67+
resp = registrytypes.DistributionInspect{
68+
Descriptor: ocispec.Descriptor{
69+
MediaType: ocispec.MediaTypeImageManifest,
70+
Digest: "sha256:9bae60c369e612488c2a089c38737277a4823a3af97ec6866c3b4ad05251bfa5",
71+
Size: 2,
72+
URLs: []string{},
73+
Annotations: map[string]string{},
74+
Data: []byte{},
75+
Platform: &ociPlatformAmd,
76+
},
77+
Platforms: []ocispec.Platform{
78+
ociPlatformAmd,
79+
ociPlatformArm,
80+
},
81+
}
82+
respJSON, err = json.Marshal(resp)
83+
Expect(err).Should(BeNil())
84+
})
85+
Context("handler", func() {
86+
It("should return inspect object and 200 status code upon success", func() {
87+
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(&resp, nil)
88+
89+
// handler should return response object with 200 status code
90+
h.inspect(rr, req)
91+
Expect(rr.Body).Should(MatchJSON(respJSON))
92+
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
93+
})
94+
It("should return 403 status code if image resolution fails due to lack of credentials", func() {
95+
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(nil, errdefs.NewUnauthenticated(fmt.Errorf("access denied")))
96+
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
97+
98+
// handler should return error message with 404 status code
99+
h.inspect(rr, req)
100+
Expect(rr.Body).Should(MatchJSON(`{"message": "access denied"}`))
101+
Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden))
102+
})
103+
It("should return 403 status code if image was not found", func() {
104+
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(nil, errdefs.NewNotFound(fmt.Errorf("no such image")))
105+
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
106+
107+
// handler should return error message with 404 status code
108+
h.inspect(rr, req)
109+
Expect(rr.Body).Should(MatchJSON(`{"message": "no such image"}`))
110+
Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden))
111+
})
112+
It("should return 500 status code if service returns an error message", func() {
113+
service.EXPECT().Inspect(gomock.Any(), name, gomock.Any()).Return(nil, fmt.Errorf("error"))
114+
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
115+
116+
// handler should return error message
117+
h.inspect(rr, req)
118+
Expect(rr.Body).Should(MatchJSON(`{"message": "error"}`))
119+
Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError))
120+
})
121+
})
122+
})

api/router/router.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/runfinch/finch-daemon/api/handlers/builder"
1919
"github.com/runfinch/finch-daemon/api/handlers/container"
20+
"github.com/runfinch/finch-daemon/api/handlers/distribution"
2021
"github.com/runfinch/finch-daemon/api/handlers/exec"
2122
"github.com/runfinch/finch-daemon/api/handlers/image"
2223
"github.com/runfinch/finch-daemon/api/handlers/network"
@@ -31,14 +32,15 @@ import (
3132

3233
// Options defines the router options to be passed into the handlers.
3334
type Options struct {
34-
Config *config.Config
35-
ContainerService container.Service
36-
ImageService image.Service
37-
NetworkService network.Service
38-
SystemService system.Service
39-
BuilderService builder.Service
40-
VolumeService volume.Service
41-
ExecService exec.Service
35+
Config *config.Config
36+
ContainerService container.Service
37+
ImageService image.Service
38+
NetworkService network.Service
39+
SystemService system.Service
40+
BuilderService builder.Service
41+
VolumeService volume.Service
42+
ExecService exec.Service
43+
DistributionService distribution.Service
4244

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

cmd/finch-daemon/router_utils.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/runfinch/finch-daemon/internal/backend"
1818
"github.com/runfinch/finch-daemon/internal/service/builder"
1919
"github.com/runfinch/finch-daemon/internal/service/container"
20+
"github.com/runfinch/finch-daemon/internal/service/distribution"
2021
"github.com/runfinch/finch-daemon/internal/service/exec"
2122
"github.com/runfinch/finch-daemon/internal/service/image"
2223
"github.com/runfinch/finch-daemon/internal/service/network"
@@ -101,14 +102,15 @@ func createRouterOptions(
101102
tarExtractor := archive.NewTarExtractor(ecc.NewExecCmdCreator(), logger)
102103

103104
return &router.Options{
104-
Config: conf,
105-
ContainerService: container.NewService(clientWrapper, ncWrapper, logger, fs, tarCreator, tarExtractor),
106-
ImageService: image.NewService(clientWrapper, ncWrapper, logger),
107-
NetworkService: network.NewService(clientWrapper, ncWrapper, logger),
108-
SystemService: system.NewService(clientWrapper, ncWrapper, logger),
109-
BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor),
110-
VolumeService: volume.NewService(ncWrapper, logger),
111-
ExecService: exec.NewService(clientWrapper, logger),
112-
NerdctlWrapper: ncWrapper,
105+
Config: conf,
106+
ContainerService: container.NewService(clientWrapper, ncWrapper, logger, fs, tarCreator, tarExtractor),
107+
ImageService: image.NewService(clientWrapper, ncWrapper, logger),
108+
NetworkService: network.NewService(clientWrapper, ncWrapper, logger),
109+
SystemService: system.NewService(clientWrapper, ncWrapper, logger),
110+
BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor),
111+
VolumeService: volume.NewService(ncWrapper, logger),
112+
ExecService: exec.NewService(clientWrapper, logger),
113+
DistributionService: distribution.NewService(clientWrapper, ncWrapper, logger),
114+
NerdctlWrapper: ncWrapper,
113115
}
114116
}

e2e/e2e_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ func TestRun(t *testing.T) {
6767
// functional test for system api
6868
tests.SystemVersion(opt)
6969
tests.SystemEvents(opt)
70+
71+
// functional test for distribution api
72+
tests.DistributionInspect(opt)
7073
})
7174

7275
gomega.RegisterFailHandler(ginkgo.Fail)

0 commit comments

Comments
 (0)