diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 420583a5..e7d5740c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/Makefile b/Makefile index 11ab1f98..0091123b 100644 --- a/Makefile +++ b/Makefile @@ -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) @@ -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 diff --git a/api/handlers/builder/build.go b/api/handlers/builder/build.go index 6d45da58..4eceac90 100644 --- a/api/handlers/builder/build.go +++ b/api/handlers/builder/build.go @@ -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, @@ -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 } @@ -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 +} diff --git a/api/handlers/builder/build_test.go b/api/handlers/builder/build_test.go index 41bb0072..84eac1e8 100644 --- a/api/handlers/builder/build_test.go +++ b/api/handlers/builder/build_test.go @@ -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) @@ -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()) }) }) }) diff --git a/api/handlers/container/container.go b/api/handlers/container/container.go index 8264a207..e0fb1218 100644 --- a/api/handlers/container/container.go +++ b/api/handlers/container/container.go @@ -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. @@ -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. diff --git a/api/handlers/container/pause.go b/api/handlers/container/pause.go new file mode 100644 index 00000000..a3eb1e3d --- /dev/null +++ b/api/handlers/container/pause.go @@ -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) +} diff --git a/api/handlers/container/pause_test.go b/api/handlers/container/pause_test.go new file mode 100644 index 00000000..22569b71 --- /dev/null +++ b/api/handlers/container/pause_test.go @@ -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)) + }) + }) +}) diff --git a/api/router/router.go b/api/router/router.go index 65066929..9c1b4728 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -5,6 +5,7 @@ package router import ( "context" + "errors" "fmt" "net/http" "os" @@ -15,6 +16,7 @@ import ( "github.com/moby/moby/api/server/httputils" "github.com/moby/moby/api/types/versions" + "github.com/open-policy-agent/opa/v1/rego" "github.com/runfinch/finch-daemon/api/handlers/builder" "github.com/runfinch/finch-daemon/api/handlers/container" "github.com/runfinch/finch-daemon/api/handlers/distribution" @@ -30,6 +32,14 @@ import ( "github.com/runfinch/finch-daemon/version" ) +var errRego = errors.New("error in rego policy file") +var errInput = errors.New("error in HTTP request") + +type inputRegoRequest struct { + Method string + Path string +} + // Options defines the router options to be passed into the handlers. type Options struct { Config *config.Config @@ -41,6 +51,7 @@ type Options struct { VolumeService volume.Service ExecService exec.Service DistributionService distribution.Service + RegoFilePath string // NerdctlWrapper wraps the interactions with nerdctl to build NerdctlWrapper *backend.NerdctlWrapper @@ -48,9 +59,16 @@ type Options struct { // New creates a new router and registers the handlers to it. Returns a handler object // The struct definitions of the HTTP responses come from https://github.com/moby/moby/tree/master/api/types. -func New(opts *Options) http.Handler { +func New(opts *Options) (http.Handler, error) { r := mux.NewRouter() r.Use(VersionMiddleware) + if opts.RegoFilePath != "" { + regoMiddleware, err := CreateRegoMiddleware(opts.RegoFilePath) + if err != nil { + return nil, err + } + r.Use(regoMiddleware) + } vr := types.VersionedRouter{Router: r} logger := flog.NewLogrus() @@ -62,7 +80,7 @@ func New(opts *Options) http.Handler { 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) + return ghandlers.LoggingHandler(os.Stderr, r), nil } // VersionMiddleware checks for the requested version of the api and makes sure it falls within the bounds @@ -90,3 +108,49 @@ func VersionMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, newReq) }) } + +// CreateRegoMiddleware dynamically parses the rego file at the path specified in options +// and allows or denies the request based on the policy. +// Will return a nil function and an error if the given file path is blank or invalid. +func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Handler, error) { + if regoFilePath == "" { + return nil, errRego + } + + query := "data.finch.authz.allow" + nr := rego.New( + rego.Load([]string{regoFilePath}, nil), + rego.Query(query), + ) + + preppedQuery, err := nr.PrepareForEval(context.Background()) + if err != nil { + return nil, err + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + input := inputRegoRequest{ + Method: r.Method, + Path: r.URL.Path, + } + + fmt.Printf("Evaluating policy rules for API request with Method = %s and Path = %s \n", input.Method, input.Path) + rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input)) + if err != nil { + response.SendErrorResponse(w, http.StatusInternalServerError, errInput) + return + } + + if !rs.Allowed() { + // need to log evaluation result in order to mitigate Repudiation threat + fmt.Printf("Evaluation result: failed, method %s not allowed for path %s \n", r.Method, r.URL.Path) + response.SendErrorResponse(w, http.StatusForbidden, + fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path)) + return + } + newReq := r.WithContext(r.Context()) + next.ServeHTTP(w, newReq) + }) + }, nil +} diff --git a/api/router/router_test.go b/api/router/router_test.go index df4f69f8..1d05cd27 100644 --- a/api/router/router_test.go +++ b/api/router/router_test.go @@ -8,6 +8,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "github.com/containerd/nerdctl/v2/pkg/config" @@ -51,8 +53,9 @@ var _ = Describe("version middleware test", func() { BuilderService: nil, VolumeService: nil, NerdctlWrapper: nil, + RegoFilePath: "", } - h = New(opts) + h, _ = New(opts) rr = httptest.NewRecorder() expected = types.VersionInfo{ Platform: struct { @@ -126,3 +129,69 @@ var _ = Describe("version middleware test", func() { Expect(v).Should(Equal(expected)) }) }) + +// Unit tests for the rego handler. +var _ = Describe("rego middleware test", func() { + var ( + opts *Options + rr *httptest.ResponseRecorder + expected types.VersionInfo + sysSvc *mocks_system.MockService + regoFilePath string + ) + + BeforeEach(func() { + mockCtrl := gomock.NewController(GinkgoT()) + defer mockCtrl.Finish() + + tempDirPath := GinkgoT().TempDir() + regoFilePath = filepath.Join(tempDirPath, "authz.rego") + os.Create(regoFilePath) + + c := config.Config{} + sysSvc = mocks_system.NewMockService(mockCtrl) + opts = &Options{ + Config: &c, + SystemService: sysSvc, + } + rr = httptest.NewRecorder() + expected = types.VersionInfo{} + sysSvc.EXPECT().GetVersion(gomock.Any()).Return(&expected, nil).AnyTimes() + }) + It("should return a 200 error for calls by default", func() { + h, err := New(opts) + Expect(err).Should(BeNil()) + + req, _ := http.NewRequest(http.MethodGet, "/version", nil) + h.ServeHTTP(rr, req) + + Expect(rr).Should(HaveHTTPStatus(http.StatusOK)) + }) + + It("should return a 400 error for disallowed calls", func() { + regoPolicy := `package finch.authz +import rego.v1 + +default allow = false` + + os.WriteFile(regoFilePath, []byte(regoPolicy), 0644) + opts.RegoFilePath = regoFilePath + h, err := New(opts) + Expect(err).Should(BeNil()) + + req, _ := http.NewRequest(http.MethodGet, "/version", nil) + h.ServeHTTP(rr, req) + + Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden)) + }) + + It("should return an error for poorly formed rego files", func() { + regoPolicy := `poorly formed rego file` + + os.WriteFile(regoFilePath, []byte(regoPolicy), 0644) + opts.RegoFilePath = regoFilePath + _, err := New(opts) + + Expect(err).Should(Not(BeNil())) + }) +}) diff --git a/cmd/finch-daemon/main.go b/cmd/finch-daemon/main.go index 314a83f8..17148049 100644 --- a/cmd/finch-daemon/main.go +++ b/cmd/finch-daemon/main.go @@ -43,12 +43,15 @@ const ( ) type DaemonOptions struct { - debug bool - socketAddr string - socketOwner int - debugAddress string - configPath string - pidFile string + debug bool + socketAddr string + socketOwner int + debugAddress string + configPath string + pidFile string + regoFilePath string + enableMiddleware bool + regoFileLock *flock.Flock } var options = new(DaemonOptions) @@ -67,6 +70,8 @@ func main() { rootCmd.Flags().StringVar(&options.debugAddress, "debug-addr", "", "") rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path") rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location") + rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path") + rootCmd.Flags().BoolVar(&options.enableMiddleware, "enable-middleware", false, "turn on middleware for allowlisting") if err := rootCmd.Execute(); err != nil { log.Printf("got error: %v", err) log.Fatal(err) @@ -144,6 +149,10 @@ func run(options *DaemonOptions) error { logger := flog.NewLogrus() r, err := newRouter(options, logger) if err != nil { + // call regoFile cleanup function here to unlock previously locked file + if options.regoFilePath != "" { + cleanupRegoFile(options, logger) + } return fmt.Errorf("failed to create a router: %w", err) } @@ -193,6 +202,8 @@ func run(options *DaemonOptions) error { } }() + defer cleanupRegoFile(options, logger) + sdNotify(daemon.SdNotifyReady, logger) serverWg.Wait() logger.Debugln("Server stopped. Exiting...") @@ -215,8 +226,20 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error return nil, err } - opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger) - return router.New(opts), nil + var regoFilePath string + if options.enableMiddleware { + regoFilePath, err = sanitizeRegoFile(options) + if err != nil { + return nil, err + } + } + + opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath) + newRouter, err := router.New(opts) + if err != nil { + return nil, err + } + return newRouter, nil } func handleSignal(socket string, server *http.Server, logger *flog.Logrus) { diff --git a/cmd/finch-daemon/router_utils.go b/cmd/finch-daemon/router_utils.go index 0261f4c4..b14756b4 100644 --- a/cmd/finch-daemon/router_utils.go +++ b/cmd/finch-daemon/router_utils.go @@ -7,11 +7,14 @@ import ( "errors" "fmt" "os" + "path/filepath" + "strings" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/pkg/namespaces" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/config" + "github.com/gofrs/flock" toml "github.com/pelletier/go-toml/v2" "github.com/runfinch/finch-daemon/api/router" "github.com/runfinch/finch-daemon/internal/backend" @@ -26,6 +29,7 @@ import ( "github.com/runfinch/finch-daemon/pkg/archive" "github.com/runfinch/finch-daemon/pkg/ecc" "github.com/runfinch/finch-daemon/pkg/flog" + "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -90,12 +94,52 @@ func createContainerdClient(conf *config.Config) (*backend.ContainerdClientWrapp return backend.NewContainerdClientWrapper(client), nil } +// sanitizeRegoFile validates and prepares the Rego policy file for use. +// It checks validates the file, acquires a file lock, +// and sets rego file to be read-only. +func sanitizeRegoFile(options *DaemonOptions) (string, error) { + if options.regoFilePath != "" { + if !options.enableMiddleware { + return "", fmt.Errorf("rego file path was provided without the --enable-middleware flag, please provide the --enable-middleware flag") // todo, can we default to setting this flag ourselves is this better UX? + } + + if err := checkRegoFileValidity(options.regoFilePath); err != nil { + return "", err + } + } + + if options.enableMiddleware && options.regoFilePath == "" { + return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag") + } + + fileLock := flock.New(options.regoFilePath) + + locked, err := fileLock.TryLock() + if err != nil { + return "", fmt.Errorf("error acquiring lock on rego file: %v", err) + } + if !locked { + return "", fmt.Errorf("unable to acquire lock on rego file, it may be in use by another process") + } + + // Change file permissions to read-only + err = os.Chmod(options.regoFilePath, 0400) + if err != nil { + fileLock.Unlock() + return "", fmt.Errorf("error changing rego file permissions: %v", err) + } + options.regoFileLock = fileLock + + return options.regoFilePath, nil +} + // createRouterOptions creates router options by initializing all required services. func createRouterOptions( conf *config.Config, clientWrapper *backend.ContainerdClientWrapper, ncWrapper *backend.NerdctlWrapper, logger *flog.Logrus, + regoFilePath string, ) *router.Options { fs := afero.NewOsFs() tarCreator := archive.NewTarCreator(ecc.NewExecCmdCreator(), logger) @@ -112,5 +156,43 @@ func createRouterOptions( ExecService: exec.NewService(clientWrapper, logger), DistributionService: distribution.NewService(clientWrapper, ncWrapper, logger), NerdctlWrapper: ncWrapper, + RegoFilePath: regoFilePath, } } + +// checkRegoFileValidity verifies that the given rego file exists and has the right file extension. +func checkRegoFileValidity(regoFilePath string) error { + fmt.Println("filepath in checkRegoFileValidity = ", regoFilePath) + if _, err := os.Stat(regoFilePath); os.IsNotExist(err) { + return fmt.Errorf("provided Rego file path does not exist: %s", regoFilePath) + } + + // Check if the file has a valid extension (.rego) + fileExt := strings.ToLower(filepath.Ext(regoFilePath)) + + fmt.Println("fileExt = ", fileExt) + if fileExt != ".rego" { + return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported") + } + + return nil +} + +func cleanupRegoFile(options *DaemonOptions, logger *flog.Logrus) { + if options.regoFileLock == nil { + return // Already cleaned up or nothing to clean + } + + // unlock the rego file + if err := options.regoFileLock.Unlock(); err != nil { + logrus.Errorf("failed to unlock Rego file: %v", err) + } + logger.Infof("rego file unlocked") + + // make rego file editable + if err := os.Chmod(options.regoFilePath, 0600); err != nil { + logrus.Errorf("failed to change file permissions of rego file: %v", err) + } + + options.regoFileLock = nil +} diff --git a/cmd/finch-daemon/router_utils_test.go b/cmd/finch-daemon/router_utils_test.go index c5ee9537..ddd585cd 100644 --- a/cmd/finch-daemon/router_utils_test.go +++ b/cmd/finch-daemon/router_utils_test.go @@ -4,10 +4,14 @@ package main import ( + "fmt" "os" + "path/filepath" "testing" "github.com/containerd/nerdctl/v2/pkg/config" + "github.com/gofrs/flock" + "github.com/runfinch/finch-daemon/pkg/flog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -71,3 +75,129 @@ namespace = "test_namespace" assert.Equal(t, "test_address", cfg.Address) assert.Equal(t, "test_namespace", cfg.Namespace) } + +func TestCleanupRegoFile(t *testing.T) { + tests := []struct { + name string + setupFunc func() (*DaemonOptions, *flog.Logrus, func()) + }{ + { + name: "successful cleanup", + setupFunc: func() (*DaemonOptions, *flog.Logrus, func()) { + tmpFile, err := os.CreateTemp("", "test.rego") + require.NoError(t, err) + + fileLock := flock.New(tmpFile.Name()) + _, err = fileLock.TryLock() + require.NoError(t, err) + + err = os.Chmod(tmpFile.Name(), 0400) + require.NoError(t, err) + + logger := flog.NewLogrus() + + cleanup := func() { + os.Remove(tmpFile.Name()) + } + + return &DaemonOptions{ + regoFilePath: tmpFile.Name(), + regoFileLock: fileLock, + }, logger, cleanup + }, + }, + { + name: "nil lock handle", + setupFunc: func() (*DaemonOptions, *flog.Logrus, func()) { + return &DaemonOptions{ + regoFileLock: nil, + }, flog.NewLogrus(), func() {} + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options, logger, cleanup := tt.setupFunc() + defer cleanup() + + cleanupRegoFile(options, logger) + + if options.regoFilePath != "" { + // Verify file permissions are restored + info, err := os.Stat(options.regoFilePath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0600), info.Mode().Perm()) + } + + // Verify lock is released + assert.Nil(t, options.regoFileLock) + }) + } +} + +func TestCheckRegoFileValidity(t *testing.T) { + tests := []struct { + name string + setupFunc func() (string, func()) + expectedError string + }{ + { + name: "valid rego file", + setupFunc: func() (string, func()) { + // Create a temporary directory + tmpDir, err := os.MkdirTemp("", "rego_test") + require.NoError(t, err) + + // Create a file with .rego extension and proper content + regoPath := filepath.Join(tmpDir, "test.rego") + regoContent := `package finch.authz + +import future.keywords.if +import rego.v1 + +default allow = false +` + fmt.Println("regopath = ", regoPath) + err = os.WriteFile(regoPath, []byte(regoContent), 0600) + require.NoError(t, err) + + return regoPath, func() { + os.RemoveAll(tmpDir) + } + }, + expectedError: "", + }, + { + name: "non-existent file", + setupFunc: func() (string, func()) { + return filepath.Join(os.TempDir(), "nonexistent.rego"), func() {} + }, + expectedError: "provided Rego file path does not exist", + }, + { + name: "wrong extension", + setupFunc: func() (string, func()) { + tmpFile, err := os.CreateTemp("", "test.txt") + require.NoError(t, err) + return tmpFile.Name(), func() { os.Remove(tmpFile.Name()) } + }, + expectedError: "invalid file extension", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath, cleanup := tt.setupFunc() + defer cleanup() + + err := checkRegoFileValidity(filePath) + + if tt.expectedError != "" { + assert.ErrorContains(t, err, tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/docs/opa-middleware.md b/docs/opa-middleware.md new file mode 100644 index 00000000..ffbf1f44 --- /dev/null +++ b/docs/opa-middleware.md @@ -0,0 +1,126 @@ +# Applying OPA authz policies + +This guide provides instructions for setting up [OPA](https://github.com/open-policy-agent/opa) authz policies with the finch-daemon. Authz policies allow users to allowlist or deny certain resources based on policy rules. + +## What Is OPA Authz implementation +Open Policy Agent (OPA) is an open-source, general-purpose policy engine that enables unified, context-aware policy enforcement across the entire stack. OPA provides a high-level declarative language, Rego, for specifying policy as code and simple APIs to offload policy decision-making from your software. + +In the current implementation, users can use OPA Rego policies to filter API requests at the Daemon level. It's important to note that the current implementation only supports allowlisting of requests. This means you can specify which requests should be allowed, and all others will be denied by default. + +## Setting up a policy + +Use the [sample rego](../docs/sample-rego-policies/default.rego) policy template to build your policy rules. + +The package name must be `finch.authz`, the daemon middleware will look for the result of the `allow` key on each API call to determine wether to allow/deny the request. +An approved request will go through without any events, a rejected request will fail with status code 403 + +Example: + +The following policy blocks all API requests made to the daemon. +``` +package finch.authz + +default allow = false + +``` +`allow` can be modified based on the business requirements for example we can prevent users from creating new containers by preventing them from accessing the create API + +``` +allow if { + not (input.Method == "POST" and input.Path == "/v1.43/containers/create") +} +``` +Use the [Rego playground](https://play.openpolicyagent.org/) to fine tune your rego policies + +## Enable OPA Middleware + +Once you are ready with your policy document, use the `--enable-middleware` flag to tell the finch-daemon to enable the OPA middleware. The daemon will then look for the policy document provided by the `--rego-file` flag. + +Note: The `--rego-file` flag is required when `--enable-middleware` is set. + +Example: +`sudo bin/finch-daemon --debug --socket-owner $UID --socket-addr /run/finch-test.sock --pidfile /run/finch-test.pid --enable-middleware --rego-file //finch-daemon/docs/sample-rego-policies/default.rego &` + + +# Best practices for secure rego policies + +## Comprehensive API Path Protection + +When writing Rego policies, use pattern matching for API paths to prevent unauthorized access. Simple string matching can be bypassed by adding prefixes to API paths. + +Consider this potentially vulnerable policy that tries to restrict access to a specific container: +``` +# INCORRECT: Can be bypassed +allow if { + not (input.Path == "/v1.43/containers/sensitive-container/json") +} +``` +This policy can be bypassed in multiple ways: +1. Using container ID instead of name: `/v1.43/containers/abc123.../json` +2. Adding path prefixes: `/custom/v1.43/containers/sensitive-container/json` + +Follow the path matching best practices below to properly secure your resources. + +## Path Matching Best Practices + +``` +package finch.authz + +import future.keywords.if +import rego.v1 + +# Use pattern matching for comprehensive path protection +is_container_api if { + glob.match("/*/containers/*", [], input.Path) +} + +is_container_create if { + input.Method == "POST" + glob.match("/*/containers/create", [], input.Path) +} + +# Protect against path variations +allow if { + not is_container_api # Blocks all container-related paths + not is_container_create # Specifically blocks container creation +} +``` +Use these [example policies](https://github.com/open-policy-agent/opa-docker-authz/blob/2c7eb5c729fca70a3e5cda6f15c2d9cc121b9481/example.rego) to build your opa policy + +Remember that only `Method` and `Path` is the only values that +gets passed to the opa middleware. + + +### Common Security Pitfalls + +- **Incomplete Path Matching**: Always use pattern matching functions like glob.match() instead of exact string matching to catch path variations. +- **Missing HTTP Methods**: Consider all HTTP methods that could access a resource (GET, POST, PUT, DELETE). +- **Alternative API Endpoints**: Be aware that some operations can be performed through multiple endpoints. + +### Monitoring and Alerting +The finch-daemon's inability to start due to policy issues could impact system operations. Implement System Service Monitoring in order to be on top of any such failures. + +### Security Recommendations +- Policy Testing + - Test policies in a non-production environment + - Use the [rego playground](https://play.openpolicyagent.org/) to test policies +- Logging and Audit + - Enable comprehensive logging of policy decisions + - Monitor for unexpected denials + + +### Critical Security Considerations : Rego Policy File Protection +The Rego policy file is a critical security control. +Any user with sudo privileges can: + +- Modify the policy file to weaken security controls +- Replace the policy with a more permissive version +- Disable policy enforcement entirely + +#### Recomended Security Controls + +- Access Controls + - Restrict sudo access to specific commands +- Monitoring + - Monitor policy file changes + - Monitor daemon service status \ No newline at end of file diff --git a/docs/sample-rego-policies/default.rego b/docs/sample-rego-policies/default.rego new file mode 100644 index 00000000..e1fa7737 --- /dev/null +++ b/docs/sample-rego-policies/default.rego @@ -0,0 +1,38 @@ +package finch.authz + +import future.keywords.if +import rego.v1 + +default allow = false + +allow if { + not is_container_create + not is_networs_api + not is_swarm_api + not is_plugins +} + +is_container_create if { + input.Method == "POST" + glob.match("/**/containers/create", ["/"], input.Path) +} + +is_networs_api if { + input.Method == "GET" + glob.match("/**/networks", ["/"], input.Path) +} + +is_swarm_api if { + input.Method == "GET" + glob.match("/**/swarm", ["/"], input.Path) +} + +is_plugins if { + input.Method == "GET" + glob.match("/**/plugins", ["/"], input.Path) +} + +is_forbidden_container if { + input.Method == "GET" + glob.match("/**/container/1f576a797a486438548377124f6cb7770a5cb7c8ff6a11c069cb4128d3f59462/json", ["/"], input.Path) +} \ No newline at end of file diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index a22b92a5..fde29726 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -5,7 +5,9 @@ package e2e import ( "flag" + "log" "os" + "strings" "testing" "github.com/onsi/ginkgo/v2" @@ -14,17 +16,39 @@ import ( "github.com/runfinch/common-tests/option" "github.com/runfinch/finch-daemon/e2e/tests" + "github.com/runfinch/finch-daemon/e2e/util" ) -// Subject defines which CLI the tests are run against, defaults to \"nerdctl\" in the user's PATH. -var Subject = flag.String("subject", "nerdctl", `which CLI the tests are run against, defaults to "nerdctl" in the user's PATH.`) +const ( + defaultNamespace = "finch" + testE2EEnv = "TEST_E2E" + middlewareE2EEnv = "MIDDLEWARE_E2E" + opaTestDescription = "Finch Daemon OPA E2E Tests" + e2eTestDescription = "Finch Daemon Functional test" +) + +var ( + Subject = flag.String("subject", "nerdctl", `which CLI the tests are run against, defaults to "nerdctl" in the user's PATH.`) + SubjectPrefix = flag.String("daemon-context-subject-prefix", "", `A string which prefixes the command the tests are run against, defaults to "". This string will be split by spaces.`) + PrefixedSubjectEnv = flag.String("daemon-context-subject-env", "", `Environment to add when running a prefixed subject, in the form of a string like "EXAMPLE=foo EXAMPLE2=bar"`) +) func TestRun(t *testing.T) { - if os.Getenv("TEST_E2E") != "1" { - t.Skip("E2E tests skipped. Set TEST_E2E=1 to run these tests") + switch { + case os.Getenv(middlewareE2EEnv) == "1": + runOPATests(t) + case os.Getenv(testE2EEnv) == "1": + runE2ETests(t) + default: + t.Skip("E2E tests skipped. Set TEST_E2E=1 to run regular E2E tests or MIDDLEWARE_E2E=1 to run OPA middleware tests") } - opt, _ := option.New([]string{*Subject, "--namespace", "finch"}) +} + +func createTestOption() (*option.Option, error) { + return option.New([]string{*Subject, "--namespace", defaultNamespace}) +} +func setupTestSuite(opt *option.Option) { ginkgo.SynchronizedBeforeSuite(func() []byte { tests.SetupLocalRegistry(opt) return nil @@ -32,51 +56,133 @@ func TestRun(t *testing.T) { ginkgo.SynchronizedAfterSuite(func() { tests.CleanupLocalRegistry(opt) - // clean up everything after the local registry is cleaned up command.RemoveAll(opt) }, func() {}) +} + +func runOPATests(t *testing.T) { + if err := parseTestFlags(); err != nil { + log.Fatal("failed to parse go test flags:", err) + } + + opt, err := createTestOption() + if err != nil { + log.Fatal("failed to create test option:", err) + } + + setupTestSuite(opt) + + ginkgo.Describe(opaTestDescription, func() { + tests.OpaMiddlewareTest(opt) + }) + + runTests(t, opaTestDescription) +} + +func runE2ETests(t *testing.T) { + if err := parseTestFlags(); err != nil { + log.Fatal("failed to parse go test flags:", err) + } + + opt, err := createTestOption() + if err != nil { + log.Fatal("failed to create test option:", err) + } + + setupTestSuite(opt) + + pOpt := createPrefixedOption() - const description = "Finch Daemon Functional test" - ginkgo.Describe(description, func() { - // functional test for container APIs - tests.ContainerCreate(opt) - tests.ContainerStart(opt) - tests.ContainerStop(opt) - tests.ContainerRestart(opt) - tests.ContainerRemove(opt) - tests.ContainerList(opt) - tests.ContainerRename(opt) - tests.ContainerStats(opt) - tests.ContainerAttach(opt) - tests.ContainerLogs(opt) - tests.ContainerKill(opt) - tests.ContainerInspect(opt) - tests.ContainerWait(opt) - - // functional test for volume APIs - tests.VolumeList(opt) - tests.VolumeInspect(opt) - tests.VolumeRemove(opt) - - // functional test for network APIs - tests.NetworkCreate(opt) - tests.NetworkRemove(opt) - tests.NetworkList(opt) - tests.NetworkInspect(opt) - - // functional test for image APIs - tests.ImageRemove(opt) - tests.ImagePush(opt) - tests.ImagePull(opt) - - // functional test for system api - tests.SystemVersion(opt) - tests.SystemEvents(opt) - - // functional test for distribution api - tests.DistributionInspect(opt) + ginkgo.Describe(e2eTestDescription, func() { + runContainerTests(opt) + runVolumeTests(opt) + runNetworkTests(opt, pOpt) + runImageTests(opt) + runSystemTests(opt) + runDistributionTests(opt) }) + runTests(t, e2eTestDescription) +} + +func createPrefixedOption() func([]string, ...option.Modifier) (*option.Option, error) { + if *SubjectPrefix == "" { + return option.New + } + + var modifiers []option.Modifier + if *PrefixedSubjectEnv != "" { + modifiers = append(modifiers, option.Env(strings.Split(*PrefixedSubjectEnv, " "))) + } + return util.WrappedOption(strings.Split(*SubjectPrefix, " "), modifiers...) +} + +func runTests(t *testing.T, description string) { gomega.RegisterFailHandler(ginkgo.Fail) ginkgo.RunSpecs(t, description) } + +// functional test for container APIs. +func runContainerTests(opt *option.Option) { + tests.ContainerCreate(opt) + tests.ContainerStart(opt) + tests.ContainerStop(opt) + tests.ContainerRestart(opt) + tests.ContainerRemove(opt) + tests.ContainerList(opt) + tests.ContainerRename(opt) + tests.ContainerStats(opt) + tests.ContainerAttach(opt) + tests.ContainerLogs(opt) + tests.ContainerKill(opt) + tests.ContainerInspect(opt) + tests.ContainerWait(opt) + tests.ContainerPause(opt) +} + +// functional test for volume APIs. +func runVolumeTests(opt *option.Option) { + tests.VolumeList(opt) + tests.VolumeInspect(opt) + tests.VolumeRemove(opt) +} + +// functional test for network APIs. +func runNetworkTests(opt *option.Option, pOpt func([]string, ...option.Modifier) (*option.Option, error)) { + tests.NetworkCreate(opt, pOpt) + tests.NetworkRemove(opt) + tests.NetworkList(opt) + tests.NetworkInspect(opt) +} + +// functional test for image APIs. +func runImageTests(opt *option.Option) { + tests.ImageRemove(opt) + tests.ImagePush(opt) + tests.ImagePull(opt) +} + +// . +func runSystemTests(opt *option.Option) { + tests.SystemVersion(opt) + tests.SystemEvents(opt) +} + +// functional test for distribution api. +func runDistributionTests(opt *option.Option) { + tests.DistributionInspect(opt) +} + +// parseTestFlags parses go test flags because pflag package ignores flags with '-test.' prefix +// Related issues: +// https://github.com/spf13/pflag/issues/63 +// https://github.com/spf13/pflag/issues/238 +func parseTestFlags() error { + var testFlags []string + for _, f := range os.Args[1:] { + if strings.HasPrefix(f, "-test.") { + testFlags = append(testFlags, f) + } + } + return flag.CommandLine.Parse(testFlags) +} diff --git a/e2e/tests/container_pause.go b/e2e/tests/container_pause.go new file mode 100644 index 00000000..8fc7698d --- /dev/null +++ b/e2e/tests/container_pause.go @@ -0,0 +1,90 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "encoding/json" + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" + + "github.com/runfinch/finch-daemon/api/response" + "github.com/runfinch/finch-daemon/e2e/client" +) + +func ContainerPause(opt *option.Option) { + Describe("pause a container", func() { + var ( + uClient *http.Client + version string + apiUrl string + ) + + BeforeEach(func() { + uClient = client.NewClient(GetDockerHostUrl()) + version = GetDockerApiVersion() + relativeUrl := fmt.Sprintf("/containers/%s/pause", testContainerName) + apiUrl = client.ConvertToFinchUrl(version, relativeUrl) + }) + + AfterEach(func() { + command.RemoveAll(opt) + }) + + It("should pause a running container", func() { + // Start a container that keeps running + command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity") + + res, err := uClient.Post(apiUrl, "application/json", nil) + Expect(err).Should(BeNil()) + Expect(res.StatusCode).Should(Equal(http.StatusNoContent)) + + // Verify container is paused + output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName) + Expect(output).Should(Equal("paused")) + }) + + It("should fail to pause a non-existent container", func() { + res, err := uClient.Post(apiUrl, "application/json", nil) + Expect(err).Should(BeNil()) + Expect(res.StatusCode).Should(Equal(http.StatusNotFound)) + + var body response.Error + err = json.NewDecoder(res.Body).Decode(&body) + Expect(err).Should(BeNil()) + }) + + It("should fail to pause a non-running container", func() { + command.Run(opt, "create", "--name", testContainerName, defaultImage, "sleep", "infinity") + + res, err := uClient.Post(apiUrl, "application/json", nil) + Expect(err).Should(BeNil()) + Expect(res.StatusCode).Should(Equal(http.StatusConflict)) + + var body response.Error + err = json.NewDecoder(res.Body).Decode(&body) + Expect(err).Should(BeNil()) + + containerShouldExist(opt, testContainerName) + }) + + It("should fail to pause an already paused container", func() { + // Start and pause the container + command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity") + command.Run(opt, "pause", testContainerName) + + res, err := uClient.Post(apiUrl, "application/json", nil) + Expect(err).Should(BeNil()) + Expect(res.StatusCode).Should(Equal(http.StatusConflict)) + + var body response.Error + err = json.NewDecoder(res.Body).Decode(&body) + Expect(err).Should(BeNil()) + }) + }) +} diff --git a/e2e/tests/network_create.go b/e2e/tests/network_create.go index ce173051..fb5172eb 100644 --- a/e2e/tests/network_create.go +++ b/e2e/tests/network_create.go @@ -17,9 +17,10 @@ import ( "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/e2e/client" + "github.com/runfinch/finch-daemon/e2e/util" ) -func NetworkCreate(opt *option.Option) { +func NetworkCreate(opt *option.Option, pOpt util.NewOpt) { Describe("create a network", func() { const ( path = "/networks/create" @@ -202,7 +203,7 @@ func NetworkCreate(opt *option.Option) { Expect(stdout).To(ContainSubstring(`"finch.network.bridge.enable_icc.ipv4": "false"`)) // check iptables rules exists - iptOpt, _ := option.New([]string{"iptables"}) + iptOpt, _ := pOpt([]string{"iptables"}) command.Run(iptOpt, "-C", "FINCH-ISOLATE-CHAIN", "-i", testBridge, "-o", testBridge, "-j", "DROP") }) @@ -226,7 +227,7 @@ func NetworkCreate(opt *option.Option) { Expect(stdout).ShouldNot(ContainSubstring(`"finch.network.bridge.enable_icc.ipv4"`)) // check iptables rules does not exist - iptOpt, _ := option.New([]string{"iptables"}) + iptOpt, _ := pOpt([]string{"iptables"}) command.RunWithoutSuccessfulExit(iptOpt, "-C", "FINCH-ISOLATE-CHAIN", "-i", testBridge, "-o", testBridge, "-j", "DROP") }) diff --git a/e2e/tests/opa_middleware.go b/e2e/tests/opa_middleware.go new file mode 100644 index 00000000..c20c027a --- /dev/null +++ b/e2e/tests/opa_middleware.go @@ -0,0 +1,120 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/option" + + "github.com/runfinch/finch-daemon/api/types" + "github.com/runfinch/finch-daemon/e2e/client" +) + +// OpaMiddlewareTest tests the OPA functionality. +func OpaMiddlewareTest(opt *option.Option) { + Describe("test opa middleware functionality", func() { + var ( + uClient *http.Client + version string + wantContainerName string + containerCreateOptions types.ContainerCreateRequest + createUrl string + unimplementedUnspecifiedUrl string + unimplementedSpecifiedUrl string + ) + BeforeEach(func() { + // create a custom client to use http over unix sockets + uClient = client.NewClient(GetDockerHostUrl()) + // get the docker api version that will be tested + version = GetDockerApiVersion() + wantContainerName = fmt.Sprintf("/%s", testContainerName) + // set default container containerCreateOptions + containerCreateOptions = types.ContainerCreateRequest{} + containerCreateOptions.Image = defaultImage + createUrl = client.ConvertToFinchUrl(version, "/containers/create") + unimplementedUnspecifiedUrl = client.ConvertToFinchUrl(version, "/secrets") + unimplementedSpecifiedUrl = client.ConvertToFinchUrl(version, "/swarm") + }) + AfterEach(func() { + command.RemoveAll(opt) + }) + It("should allow GET version API request", func() { + res, err := uClient.Get(client.ConvertToFinchUrl("", "/version")) + Expect(err).ShouldNot(HaveOccurred()) + jd := json.NewDecoder(res.Body) + var v types.VersionInfo + err = jd.Decode(&v) + Expect(err).ShouldNot(HaveOccurred()) + Expect(v.Version).ShouldNot(BeNil()) + Expect(v.ApiVersion).Should(Equal("1.43")) + fmt.Println(version) + }) + + It("shold allow GET containers API request", func() { + id := command.StdoutStr(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity") + want := []types.ContainerListItem{ + { + Id: id[:12], + Names: []string{wantContainerName}, + }, + } + + res, err := uClient.Get(client.ConvertToFinchUrl(version, "/containers/json")) + Expect(err).Should(BeNil()) + Expect(res.StatusCode).Should(Equal(http.StatusOK)) + var got []types.ContainerListItem + err = json.NewDecoder(res.Body).Decode(&got) + Expect(err).Should(BeNil()) + Expect(len(got)).Should(Equal(2)) + got = filterContainerList(got) + Expect(got).Should(ContainElements(want)) + }) + + It("shold disallow POST containers/create API request", func() { + containerCreateOptions.Cmd = []string{"echo", "hello world"} + + reqBody, err := json.Marshal(containerCreateOptions) + Expect(err).Should(BeNil()) + + fmt.Println("createUrl = ", createUrl) + res, _ := uClient.Post(createUrl, "application/json", bytes.NewReader(reqBody)) + + Expect(res.StatusCode).Should(Equal(http.StatusForbidden)) + }) + + It("should not allow updates to the rego file", func() { + regoFilePath := os.Getenv("REGO_FILE_PATH") + Expect(regoFilePath).NotTo(BeEmpty(), "REGO_FILE_PATH environment variable should be set") + + fileInfo, err := os.Stat(regoFilePath) + Expect(err).NotTo(HaveOccurred(), "Failed to get Rego file info") + + // Check file permissions + mode := fileInfo.Mode() + Expect(mode.Perm()).To(Equal(os.FileMode(0400)), "Rego file should be read-only (0400)") + }) + + It("should fail unimplemented API calls, fail via daemon", func() { + fmt.Println("incompatibleUrl = ", unimplementedUnspecifiedUrl) + res, _ := uClient.Get(unimplementedUnspecifiedUrl) + + Expect(res.StatusCode).Should(Equal(http.StatusNotFound)) + }) + + It("should fail non implemented API calls,even if specified in the rego file", func() { + fmt.Println("incompatibleUrl = ", unimplementedSpecifiedUrl) + res, _ := uClient.Get(unimplementedSpecifiedUrl) + + Expect(res.StatusCode).Should(Equal(http.StatusNotFound)) + }) + }) +} diff --git a/e2e/util/option_wrapper.go b/e2e/util/option_wrapper.go new file mode 100644 index 00000000..5e285173 --- /dev/null +++ b/e2e/util/option_wrapper.go @@ -0,0 +1,23 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package testutil holds code that may be useful for any of the e2e subpackages (including e2e itself). +// It is useful to avoid import loops between the various e2e test pacakges. +package util + +import ( + "github.com/runfinch/common-tests/option" +) + +// NewOpt is a helper to make it easier for functions to accept wrapped option creators. +type NewOpt func(subject []string, modifiers ...option.Modifier) (*option.Option, error) + +// WrappedOption allows injection of new prefixed option creator function into tests. +// This is useful for scenarios where CLI commands must be run in an environment which is +// not the same as the system running the tests, like inside a SSH shell. +func WrappedOption(prefix []string, wModifiers ...option.Modifier) NewOpt { + return func(subject []string, modifiers ...option.Modifier) (*option.Option, error) { + prefix = append(prefix, subject...) + return option.New(prefix, append(wModifiers, modifiers...)...) + } +} diff --git a/go.mod b/go.mod index cec74f9f..eaa12151 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,29 @@ require ( google.golang.org/protobuf v1.36.5 ) +require ( + github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/tchap/go-patricia/v2 v2.3.2 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 // indirect @@ -115,11 +138,11 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect + github.com/open-policy-agent/opa v1.1.0 github.com/opencontainers/selinux v1.11.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rootless-containers/bypass4netns v0.4.2 // indirect github.com/rootless-containers/rootlesskit/v2 v2.3.4 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect @@ -131,19 +154,19 @@ require ( github.com/vbatts/tar-split v0.11.6 // indirect github.com/yuchanns/srslog v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.8.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/grpc v1.69.4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.70.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index d2fab0d6..7de19086 100644 --- a/go.sum +++ b/go.sum @@ -14,9 +14,23 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -86,6 +100,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= @@ -100,6 +120,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -110,12 +132,18 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluent/fluent-logger-golang v1.9.0 h1:zUdY44CHX2oIUc7VTNZc+4m+ORuO/mldQDA7czhWXEg= github.com/fluent/fluent-logger-golang v1.9.0/go.mod h1:2/HCT/jTy78yGyeNGQLGQsjF3zzzAuy6Xlk6FCMV5eU= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getlantern/httptest v0.0.0-20161025015934-4b40f4c7e590 h1:OhyiFx+yBN30O3IHrIq+9LAEhy6o7fin21wUQxF8NiE= github.com/getlantern/httptest v0.0.0-20161025015934-4b40f4c7e590/go.mod h1:rE/jidqqHHG9sjSxC24Gd5YCfZ1AT91C2wjJ28TAOfA= github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848 h1:2MhMMVBTnaHrst6HyWFDhwQCaJ05PZuOv1bE2gN8WFY= github.com/getlantern/mockconn v0.0.0-20200818071412-cb30d065a848/go.mod h1:+F5GJ7qGpQ03DBtcOEyQpM30ix4BLswdaojecFtsdy8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -129,6 +157,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -154,12 +184,15 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= @@ -171,6 +204,8 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= @@ -195,6 +230,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -243,10 +280,14 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/open-policy-agent/opa v1.1.0 h1:HMz2evdEMTyNqtdLjmu3Vyx06BmhNYAx67Yz3Ll9q2s= +github.com/open-policy-agent/opa v1.1.0/go.mod h1:T1pASQ1/vwfTa+e2fYcfpLCvWgYtqtiUv+IuA/dLPQs= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -266,11 +307,19 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBCQ3eKzKQgSW5DtlXFQJyJMAQk= github.com/rootless-containers/rootlesskit/v2 v2.3.4 h1:EHiqqiq+ntTfdnQtIgDR3etiuqKkRCPr1qpoizJxW/E= @@ -305,6 +354,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY= github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= @@ -322,6 +373,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk= github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -330,18 +383,26 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -370,6 +431,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -450,8 +513,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -476,15 +539,18 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -511,3 +577,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/backend/container.go b/internal/backend/container.go index 6d66f5fa..a87f7188 100644 --- a/internal/backend/container.go +++ b/internal/backend/container.go @@ -37,6 +37,7 @@ type NerdctlContainerSvc interface { RenameContainer(ctx context.Context, container containerd.Container, newName string, options types.ContainerRenameOptions) error KillContainer(ctx context.Context, cid string, options types.ContainerKillOptions) error ContainerWait(ctx context.Context, cid string, options types.ContainerWaitOptions) error + PauseContainer(ctx context.Context, cid string, options types.ContainerPauseOptions) error // Mocked functions for container attach GetDataStore() (string, error) @@ -130,6 +131,10 @@ func (w *NerdctlWrapper) ContainerWait(ctx context.Context, cid string, options return container.Wait(ctx, w.clientWrapper.client, []string{cid}, options) } +func (w *NerdctlWrapper) PauseContainer(ctx context.Context, cid string, options types.ContainerPauseOptions) error { + return container.Pause(ctx, w.clientWrapper.client, []string{cid}, options) +} + func (w *NerdctlWrapper) GetNerdctlExe() (string, error) { if w.nerdctlExe != "" { return w.nerdctlExe, nil diff --git a/internal/service/container/pause.go b/internal/service/container/pause.go new file mode 100644 index 00000000..3c25d9ae --- /dev/null +++ b/internal/service/container/pause.go @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "context" + "fmt" + + containerd "github.com/containerd/containerd/v2/client" + cerrdefs "github.com/containerd/errdefs" + ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types" + + "github.com/runfinch/finch-daemon/pkg/errdefs" +) + +func (s *service) Pause(ctx context.Context, cid string, options ncTypes.ContainerPauseOptions) error { + cont, err := s.getContainer(ctx, cid) + if err != nil { + if cerrdefs.IsNotFound(err) { + return errdefs.NewNotFound(err) + } + return err + } + status := s.client.GetContainerStatus(ctx, cont) + if status != containerd.Running { + if status == containerd.Paused { + return errdefs.NewConflict(fmt.Errorf("container %s is already paused", cid)) + } + return errdefs.NewConflict(fmt.Errorf("container %s is not running", cid)) + } + + return s.nctlContainerSvc.PauseContainer(ctx, cid, options) +} diff --git a/internal/service/container/pause_test.go b/internal/service/container/pause_test.go new file mode 100644 index 00000000..fe1356a6 --- /dev/null +++ b/internal/service/container/pause_test.go @@ -0,0 +1,111 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "context" + "errors" + "fmt" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + containerd "github.com/containerd/containerd/v2/client" + cerrdefs "github.com/containerd/errdefs" + ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/runfinch/finch-daemon/mocks/mocks_backend" + "github.com/runfinch/finch-daemon/mocks/mocks_container" + "github.com/runfinch/finch-daemon/mocks/mocks_logger" + "github.com/runfinch/finch-daemon/pkg/errdefs" +) + +var _ = Describe("Container Pause API", func() { + var ( + ctx context.Context + mockCtrl *gomock.Controller + logger *mocks_logger.Logger + cdClient *mocks_backend.MockContainerdClient + ncContainerSvc *mocks_backend.MockNerdctlContainerSvc + ncNetworkSvc *mocks_backend.MockNerdctlNetworkSvc + svc *service + cid string + con *mocks_container.MockContainer + pauseOptions ncTypes.ContainerPauseOptions + ) + + BeforeEach(func() { + ctx = context.Background() + mockCtrl = gomock.NewController(GinkgoT()) + logger = mocks_logger.NewLogger(mockCtrl) + cdClient = mocks_backend.NewMockContainerdClient(mockCtrl) + ncContainerSvc = mocks_backend.NewMockNerdctlContainerSvc(mockCtrl) + ncNetworkSvc = mocks_backend.NewMockNerdctlNetworkSvc(mockCtrl) + + cid = "test-container-id" + pauseOptions = ncTypes.ContainerPauseOptions{} + con = mocks_container.NewMockContainer(mockCtrl) + con.EXPECT().ID().Return(cid).AnyTimes() + + svc = &service{ + client: cdClient, + nctlContainerSvc: mockNerdctlService{ncContainerSvc, ncNetworkSvc}, + logger: logger, + } + }) + + AfterEach(func() { + mockCtrl.Finish() + }) + + Context("Pause API", func() { + It("should successfully pause a running container", func() { + cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return( + []containerd.Container{con}, nil) + cdClient.EXPECT().GetContainerStatus(gomock.Any(), gomock.Any()).Return(containerd.Running) + ncContainerSvc.EXPECT().PauseContainer(ctx, cid, pauseOptions).Return(nil) + + err := svc.Pause(ctx, cid, pauseOptions) + Expect(err).Should(BeNil()) + }) + + It("should return NotFound error if container is not found", func() { + mockErr := cerrdefs.ErrNotFound.WithMessage(fmt.Sprintf("no such container: %s", cid)) + cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return(nil, mockErr) + logger.EXPECT().Errorf("failed to search container: %s. error: %s", cid, mockErr.Error()) + + err := svc.Pause(ctx, cid, pauseOptions) + Expect(err.Error()).Should(Equal(errdefs.NewNotFound(fmt.Errorf("no such container: %s", cid)).Error())) + }) + + It("should return a Conflict error if container is already paused", func() { + cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return( + []containerd.Container{con}, nil) + cdClient.EXPECT().GetContainerStatus(gomock.Any(), gomock.Any()).Return(containerd.Paused) + + err := svc.Pause(ctx, cid, pauseOptions) + Expect(err.Error()).Should(Equal(errdefs.NewConflict(fmt.Errorf("container %s is already paused", cid)).Error())) + }) + + It("should return a Conflict error if container is not running", func() { + cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return( + []containerd.Container{con}, nil) + cdClient.EXPECT().GetContainerStatus(gomock.Any(), gomock.Any()).Return(containerd.Stopped) + + err := svc.Pause(ctx, cid, pauseOptions) + Expect(err.Error()).Should(Equal(errdefs.NewConflict(fmt.Errorf("container %s is not running", cid)).Error())) + }) + + It("should return a generic error if pause operation fails", func() { + cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return( + []containerd.Container{con}, nil) + cdClient.EXPECT().GetContainerStatus(gomock.Any(), gomock.Any()).Return(containerd.Running) + mockErr := errors.New("generic error while pausing container") + ncContainerSvc.EXPECT().PauseContainer(ctx, cid, pauseOptions).Return(mockErr) + + err := svc.Pause(ctx, cid, pauseOptions) + Expect(err).Should(Equal(mockErr)) + }) + }) +}) diff --git a/mocks/mocks_backend/nerdctlcontainersvc.go b/mocks/mocks_backend/nerdctlcontainersvc.go index 0d49017d..2313e953 100644 --- a/mocks/mocks_backend/nerdctlcontainersvc.go +++ b/mocks/mocks_backend/nerdctlcontainersvc.go @@ -207,6 +207,20 @@ func (mr *MockNerdctlContainerSvcMockRecorder) NewNetworkingOptionsManager(arg0 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewNetworkingOptionsManager", reflect.TypeOf((*MockNerdctlContainerSvc)(nil).NewNetworkingOptionsManager), arg0) } +// PauseContainer mocks base method. +func (m *MockNerdctlContainerSvc) PauseContainer(arg0 context.Context, arg1 string, arg2 types.ContainerPauseOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PauseContainer", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// PauseContainer indicates an expected call of PauseContainer. +func (mr *MockNerdctlContainerSvcMockRecorder) PauseContainer(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseContainer", reflect.TypeOf((*MockNerdctlContainerSvc)(nil).PauseContainer), arg0, arg1, arg2) +} + // RemoveContainer mocks base method. func (m *MockNerdctlContainerSvc) RemoveContainer(arg0 context.Context, arg1 client.Container, arg2, arg3 bool) error { m.ctrl.T.Helper() diff --git a/mocks/mocks_container/containersvc.go b/mocks/mocks_container/containersvc.go index c93e682b..a84cd9d3 100644 --- a/mocks/mocks_container/containersvc.go +++ b/mocks/mocks_container/containersvc.go @@ -170,6 +170,20 @@ func (mr *MockServiceMockRecorder) Logs(arg0, arg1, arg2 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockService)(nil).Logs), arg0, arg1, arg2) } +// Pause mocks base method. +func (m *MockService) Pause(arg0 context.Context, arg1 string, arg2 types.ContainerPauseOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Pause", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Pause indicates an expected call of Pause. +func (mr *MockServiceMockRecorder) Pause(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockService)(nil).Pause), arg0, arg1, arg2) +} + // Remove mocks base method. func (m *MockService) Remove(arg0 context.Context, arg1 string, arg2, arg3 bool) error { m.ctrl.T.Helper()