Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,13 @@ jobs:
run: sudo bin/finch-daemon --debug --socket-owner $UID &
- name: Run e2e test
run: sudo make test-e2e
- name: Clean up Daemon socket
run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid
- name: Verify Rego file presence
run: ls -l ${{ github.workspace }}/docs/sample-rego-policies/default.rego
- name: Set Rego file path
run: echo "REGO_FILE_PATH=${{ github.workspace }}/docs/sample-rego-policies/default.rego" >> $GITHUB_ENV
- name: Start finch-daemon with opa Authz
run: sudo bin/finch-daemon --debug --enable-middleware --rego-file ${{ github.workspace }}/docs/sample-rego-policies/default.rego --socket-owner $UID &
- name: Run opa e2e tests
run: sudo -E make test-e2e-opa
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

GINKGO = go run github.com/onsi/ginkgo/v2/ginkgo
# Common ginkgo options: -v for verbose mode, --focus="test name" for running single tests
GFLAGS ?= --race --randomize-all --randomize-suites --vv
GFLAGS ?= --race --randomize-all --randomize-suites
BIN = $(PWD)/bin
FINCH_DAEMON_PROJECT_ROOT ?= $(shell pwd)

Expand Down Expand Up @@ -114,6 +114,14 @@ test-e2e: linux
TEST_E2E=1 \
$(GINKGO) $(GFLAGS) ./e2e/...

.PHONY: test-e2e-opa
test-e2e-opa: linux
DOCKER_HOST="unix:///run/finch.sock" \
DOCKER_API_VERSION="v1.43" \
MIDDLEWARE_E2E=1 \
TEST_E2E=1 \
$(GINKGO) $(GFLAGS) ./e2e/...

.PHONY: licenses
licenses:
PATH=$(BIN):$(PATH) go-licenses report --template="scripts/third-party-license.tpl" --ignore github.com/runfinch ./... > THIRD_PARTY_LICENSES
Expand Down
47 changes: 38 additions & 9 deletions api/handlers/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ func (h *handler) getBuildOptions(w http.ResponseWriter, r *http.Request, stream
h.logger.Warnf("Failed to get buildkit host: %v", err.Error())
return nil, err
}

buildArgs, err := getQueryParamMap(r, "buildargs", []string{})
if err != nil {
return nil, fmt.Errorf("unable to parse buildargs query: %s", err)
}

labels, err := getQueryParamMap(r, "labels", []string{})
if err != nil {
return nil, fmt.Errorf("unable to parse labels query: %s", err)
}

cacheFrom, err := getQueryParamMap(r, "cachefrom", []string{})
if err != nil {
return nil, fmt.Errorf("unable to parse cacheFrom query: %s", err)
}

options := types.BuilderBuildOptions{
// TODO: investigate - interestingly nerdctl prints all the build log in stderr for some reason.
Stdout: stream,
Expand All @@ -73,17 +89,15 @@ func (h *handler) getBuildOptions(w http.ResponseWriter, r *http.Request, stream
Platform: getQueryParamList(r, "platform", []string{}),
Rm: getQueryParamBool(r, "rm", true),
Progress: "auto",
Quiet: getQueryParamBool(r, "q", true),
NoCache: getQueryParamBool(r, "nocache", false),
CacheFrom: cacheFrom,
BuildArgs: buildArgs,
Label: labels,
NetworkMode: getQueryParamStr(r, "networkmode", ""),
Output: getQueryParamStr(r, "output", ""),
}

argsQuery := r.URL.Query().Get("buildargs")
if argsQuery != "" {
buildargs := make(map[string]string)
err := json.Unmarshal([]byte(argsQuery), &buildargs)
if err != nil {
return nil, fmt.Errorf("unable to parse buildargs query: %s", err)
}
options.BuildArgs = maputility.Flatten(buildargs, maputility.KeyEqualsValueFormat)
}
return &options, nil
}

Expand Down Expand Up @@ -117,3 +131,18 @@ func getQueryParamList(r *http.Request, paramName string, defaultValue []string)
}
return params[paramName]
}

func getQueryParamMap(r *http.Request, paramName string, defaultValue []string) ([]string, error) {
query := r.URL.Query().Get(paramName)
if query == "" {
return defaultValue, nil
}

var parsedMap map[string]string
err := json.Unmarshal([]byte(query), &parsedMap)
if err != nil {
return nil, fmt.Errorf("unable to parse %s query: %s", paramName, err)
}

return maputility.Flatten(parsedMap, maputility.KeyEqualsValueFormat), nil
}
61 changes: 61 additions & 0 deletions api/handlers/builder/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,60 @@ var _ = Describe("Build API", func() {
Expect(err).Should(BeNil())
Expect(buildOption.Rm).Should(BeTrue())
})
It("should set the q query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?q=false", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.Quiet).Should(BeFalse())
})
It("should set the nocache query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?nocache=true", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.NoCache).Should(BeTrue())
})
It("should set the CacheFrom query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?cachefrom={\"image1\":\"tag1\",\"image2\":\"tag2\"}", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.CacheFrom).Should(ContainElements("image1=tag1", "image2=tag2"))
})

It("should set the BuildArgs query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?buildargs={\"ARG1\":\"value1\",\"ARG2\":\"value2\"}", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.BuildArgs).Should(ContainElements("ARG1=value1", "ARG2=value2"))
})

It("should set the Label query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?labels={\"LABEL1\":\"value1\",\"LABEL2\":\"value2\"}", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.Label).Should(ContainElements("LABEL1=value1", "LABEL2=value2"))
})

It("should set the NetworkMode query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?networkmode=host", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.NetworkMode).Should(Equal("host"))
})

It("should set the Output query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build?output=type=docker", nil)
buildOption, err := h.getBuildOptions(rr, req, stream)
Expect(err).Should(BeNil())
Expect(buildOption.Output).Should(Equal("type=docker"))
})

It("should set all the default value for the query param", func() {
ncBuildSvc.EXPECT().GetBuildkitHost().Return("mocked-value", nil).AnyTimes()
req = httptest.NewRequest(http.MethodPost, "/build", nil)
Expand All @@ -162,6 +216,13 @@ var _ = Describe("Build API", func() {
Expect(buildOption.Platform).Should(HaveLen(0))
Expect(buildOption.File).Should(Equal("Dockerfile"))
Expect(buildOption.Rm).Should(BeTrue())
Expect(buildOption.Quiet).Should(BeTrue())
Expect(buildOption.NoCache).Should(BeFalse())
Expect(buildOption.CacheFrom).Should(BeEmpty())
Expect(buildOption.BuildArgs).Should(BeEmpty())
Expect(buildOption.Label).Should(BeEmpty())
Expect(buildOption.NetworkMode).Should(BeEmpty())
Expect(buildOption.Output).Should(BeEmpty())
})
})
})
2 changes: 2 additions & 0 deletions api/handlers/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Service interface {
Stats(ctx context.Context, cid string) (<-chan *types.StatsJSON, error)
ExecCreate(ctx context.Context, cid string, config types.ExecConfig) (string, error)
Kill(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error
Pause(ctx context.Context, cid string, options ncTypes.ContainerPauseOptions) error
}

// RegisterHandlers register all the supported endpoints related to the container APIs.
Expand All @@ -60,6 +61,7 @@ func RegisterHandlers(r types.VersionedRouter, service Service, conf *config.Con
r.HandleFunc("/{id:.*}/stats", h.stats, http.MethodGet)
r.HandleFunc("/{id:.*}/exec", h.exec, http.MethodPost)
r.HandleFunc("/{id:.*}/kill", h.kill, http.MethodPost)
r.HandleFunc("/{id:.*}/pause", h.pause, http.MethodPost)
}

// newHandler creates the handler that serves all the container related APIs.
Expand Down
57 changes: 57 additions & 0 deletions api/handlers/container/pause.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package container

import (
"net/http"
"os"

"github.com/containerd/containerd/v2/pkg/namespaces"
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/gorilla/mux"

"github.com/runfinch/finch-daemon/api/response"
"github.com/runfinch/finch-daemon/pkg/errdefs"
)

// pause pauses a running container.
func (h *handler) pause(w http.ResponseWriter, r *http.Request) {
cid, ok := mux.Vars(r)["id"]
if !ok || cid == "" {
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg("must specify a container ID"))
return
}

ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)

devNull, err := os.OpenFile("/dev/null", os.O_WRONLY, 0600)
if err != nil {
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg("failed to open /dev/null"))
return
}
defer devNull.Close()

globalOpt := ncTypes.GlobalCommandOptions(*h.Config)
options := ncTypes.ContainerPauseOptions{
GOptions: globalOpt,
Stdout: devNull,
}

err = h.service.Pause(ctx, cid, options)
if err != nil {
var code int
switch {
case errdefs.IsNotFound(err):
code = http.StatusNotFound
case errdefs.IsConflict(err):
code = http.StatusConflict
default:
code = http.StatusInternalServerError
}
response.JSON(w, code, response.NewError(err))
return
}

response.Status(w, http.StatusNoContent)
}
110 changes: 110 additions & 0 deletions api/handlers/container/pause_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package container

import (
"context"
"fmt"
"net/http"
"net/http/httptest"

ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/config"
"github.com/golang/mock/gomock"
"github.com/gorilla/mux"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/runfinch/finch-daemon/pkg/errdefs"

"github.com/runfinch/finch-daemon/mocks/mocks_container"
"github.com/runfinch/finch-daemon/mocks/mocks_logger"
)

var _ = Describe("Container Pause API", func() {
var (
mockCtrl *gomock.Controller
logger *mocks_logger.Logger
service *mocks_container.MockService
h *handler
rr *httptest.ResponseRecorder
_ ncTypes.GlobalCommandOptions
_ error
)

BeforeEach(func() {
mockCtrl = gomock.NewController(GinkgoT())
defer mockCtrl.Finish()
logger = mocks_logger.NewLogger(mockCtrl)
service = mocks_container.NewMockService(mockCtrl)
c := config.Config{}
h = newHandler(service, &c, logger)
rr = httptest.NewRecorder()
})

Context("pause handler", func() {
It("should return 204 No Content on successful pause", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/pause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Pause(gomock.Any(), "id1", gomock.Any()).DoAndReturn(
func(ctx context.Context, cid string, opts ncTypes.ContainerPauseOptions) error {
return nil
})

h.pause(rr, req)
Expect(rr.Body.String()).Should(BeEmpty())
Expect(rr).Should(HaveHTTPStatus(http.StatusNoContent))
})

It("should return 400 when container ID is missing", func() {
req, err := http.NewRequest(http.MethodPost, "/containers//pause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": ""})

h.pause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "must specify a container ID"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusBadRequest))
})

It("should return 404 when service returns a not found error", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/pause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Pause(gomock.Any(), "id1", gomock.Any()).Return(
errdefs.NewNotFound(fmt.Errorf("container not found")))

h.pause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "container not found"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusNotFound))
})

It("should return 409 when service returns a conflict error", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/pause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Pause(gomock.Any(), "id1", gomock.Any()).Return(
errdefs.NewConflict(fmt.Errorf("container already paused")))

h.pause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "container already paused"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusConflict))
})

It("should return 500 when service returns an internal error", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/pause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Pause(gomock.Any(), "id1", gomock.Any()).Return(
fmt.Errorf("unexpected internal error"))

h.pause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "unexpected internal error"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError))
})
})
})
Loading
Loading