This repository has been archived by the owner on Nov 27, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Francesco Ilario <filario@redhat.com>
- Loading branch information
Showing
3 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package workspace | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
|
||
"k8s.io/apimachinery/pkg/types" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
jsonpatch "github.com/evanphx/json-patch/v5" | ||
ccontext "github.com/konflux-workspaces/workspaces/server/core/context" | ||
"github.com/konflux-workspaces/workspaces/server/log" | ||
|
||
restworkspacesv1alpha1 "github.com/konflux-workspaces/workspaces/server/api/v1alpha1" | ||
workspacesv1alpha1 "github.com/konflux-workspaces/workspaces/server/api/v1alpha1" | ||
) | ||
|
||
// PatchWorkspaceCommand contains the information needed to retrieve a Workspace the user has access to from the data source | ||
type PatchWorkspaceCommand struct { | ||
Owner string | ||
Workspace string | ||
Patch []byte | ||
PatchType types.PatchType | ||
} | ||
|
||
// PatchWorkspaceResponse contains the workspace the user requested | ||
type PatchWorkspaceResponse struct { | ||
Workspace *restworkspacesv1alpha1.Workspace | ||
} | ||
|
||
// PatchWorkspaceHandler processes PatchWorkspaceCommand and returns PatchWorkspaceResponse fetching data from a WorkspacePatcher | ||
type PatchWorkspaceHandler struct { | ||
reader WorkspaceReader | ||
updater WorkspaceUpdater | ||
} | ||
|
||
// NewPatchWorkspaceHandler creates a new PatchWorkspaceHandler that uses a specified WorkspacePatcher | ||
func NewPatchWorkspaceHandler(reader WorkspaceReader, updater WorkspaceUpdater) *PatchWorkspaceHandler { | ||
return &PatchWorkspaceHandler{ | ||
reader: reader, | ||
updater: updater, | ||
} | ||
} | ||
|
||
// Handle handles a PatchWorkspaceCommand and returns a PatchWorkspaceResponse or an error | ||
func (h *PatchWorkspaceHandler) Handle(ctx context.Context, command PatchWorkspaceCommand) (*PatchWorkspaceResponse, error) { | ||
// authorization | ||
// If required, implement here complex logic like multiple-domains filtering, etc | ||
u, ok := ctx.Value(ccontext.UserSignupComplaintNameKey).(string) | ||
if !ok { | ||
return nil, fmt.Errorf("unauthenticated request") | ||
} | ||
|
||
// validate query | ||
// TODO: sanitize input, block reserved labels, etc | ||
|
||
// retrieve workspace | ||
w := workspacesv1alpha1.Workspace{} | ||
if err := h.reader.ReadUserWorkspace(ctx, u, command.Owner, command.Workspace, &w); err != nil { | ||
return nil, err | ||
} | ||
|
||
// apply patch | ||
pw, err := h.applyPatch(&w, command) | ||
if err != nil { | ||
return nil, fmt.Errorf("error patching Workspace %s/%s: %w", command.Owner, command.Workspace, err) | ||
} | ||
|
||
log.FromContext(ctx).Debug("updating workspace", "workspace", pw) | ||
opts := &client.UpdateOptions{} | ||
if err := h.updater.UpdateUserWorkspace(ctx, u, pw, opts); err != nil { | ||
return nil, err | ||
} | ||
|
||
// reply | ||
return &PatchWorkspaceResponse{ | ||
Workspace: pw, | ||
}, nil | ||
} | ||
|
||
func (h *PatchWorkspaceHandler) applyPatch(w *workspacesv1alpha1.Workspace, command PatchWorkspaceCommand) (*workspacesv1alpha1.Workspace, error) { | ||
if command.PatchType != types.MergePatchType { | ||
return nil, fmt.Errorf("unsupported patch type: %s", command.PatchType) | ||
} | ||
|
||
// marshal workspace as json | ||
wj, err := json.Marshal(w) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// apply jsonpatch | ||
pwj, err := jsonpatch.MergePatch(wj, []byte(command.Patch)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// unmarshal json to struct | ||
pw := workspacesv1alpha1.Workspace{} | ||
if err := json.Unmarshal(pwj, &pw); err != nil { | ||
return nil, err | ||
} | ||
|
||
return &pw, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package workspace_test | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
"go.uber.org/mock/gomock" | ||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/types" | ||
|
||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
ccontext "github.com/konflux-workspaces/workspaces/server/core/context" | ||
"github.com/konflux-workspaces/workspaces/server/core/workspace" | ||
|
||
workspacesv1alpha1 "github.com/konflux-workspaces/workspaces/server/api/v1alpha1" | ||
) | ||
|
||
var _ = Describe("", func() { | ||
var ( | ||
ctrl *gomock.Controller | ||
ctx context.Context | ||
reader *MockWorkspaceReader | ||
updater *MockWorkspaceUpdater | ||
request workspace.PatchWorkspaceCommand | ||
handler workspace.PatchWorkspaceHandler | ||
w workspacesv1alpha1.Workspace | ||
) | ||
|
||
BeforeEach(func() { | ||
ctrl = gomock.NewController(GinkgoT()) | ||
ctx = context.Background() | ||
w = workspacesv1alpha1.Workspace{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Name: "default", | ||
Namespace: "user", | ||
}, | ||
Spec: workspacesv1alpha1.WorkspaceSpec{ | ||
Visibility: workspacesv1alpha1.WorkspaceVisibilityPrivate, | ||
}, | ||
} | ||
updater = NewMockWorkspaceUpdater(ctrl) | ||
reader = NewMockWorkspaceReader(ctrl) | ||
request = workspace.PatchWorkspaceCommand{ | ||
Workspace: w.Name, | ||
Owner: w.Namespace, | ||
} | ||
handler = *workspace.NewPatchWorkspaceHandler(reader, updater) | ||
}) | ||
|
||
AfterEach(func() { ctrl.Finish() }) | ||
|
||
It("should not allow unauthenticated requests", func() { | ||
// don't set the "user" value within ctx | ||
|
||
response, err := handler.Handle(ctx, request) | ||
Expect(err).To(HaveOccurred()) | ||
Expect(err).To(Equal(fmt.Errorf("unauthenticated request"))) | ||
Expect(response).To(BeNil()) | ||
}) | ||
|
||
It("should allow authenticated requests", func() { | ||
// given | ||
request.PatchType = types.MergePatchType | ||
request.Patch = []byte(`{"spec":{"visibility":"community"}}`) | ||
username := "foo" | ||
ctx := context.WithValue(ctx, ccontext.UserSignupComplaintNameKey, username) | ||
opts := &client.UpdateOptions{} | ||
reader.EXPECT(). | ||
ReadUserWorkspace(ctx, username, w.Namespace, w.Name, gomock.Any(), gomock.Any()). | ||
DoAndReturn(func(ctx context.Context, user, owner, workspace string, rw *workspacesv1alpha1.Workspace, opts ...client.GetOption) error { | ||
w.DeepCopyInto(rw) | ||
return nil | ||
}) | ||
updater.EXPECT(). | ||
UpdateUserWorkspace(ctx, username, gomock.Any(), opts). | ||
Return(nil) | ||
|
||
// when | ||
response, err := handler.Handle(ctx, request) | ||
|
||
// then | ||
Expect(err).NotTo(HaveOccurred()) | ||
Expect(response).NotTo(BeNil()) | ||
expectedWorkspace := w.DeepCopy() | ||
expectedWorkspace.Spec.Visibility = workspacesv1alpha1.WorkspaceVisibilityCommunity | ||
Expect(response.Workspace).To(BeEquivalentTo(expectedWorkspace)) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package workspace | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/konflux-workspaces/workspaces/server/core" | ||
"github.com/konflux-workspaces/workspaces/server/core/workspace" | ||
"github.com/konflux-workspaces/workspaces/server/log" | ||
"github.com/konflux-workspaces/workspaces/server/rest/header" | ||
"github.com/konflux-workspaces/workspaces/server/rest/marshal" | ||
"k8s.io/apimachinery/pkg/types" | ||
) | ||
|
||
var ( | ||
_ http.Handler = &PatchWorkspaceHandler{} | ||
|
||
_ PatchWorkspaceMapperFunc = MapPatchWorkspaceHttp | ||
) | ||
|
||
// handler dependencies | ||
type PatchWorkspaceMapperFunc func(*http.Request, marshal.UnmarshalerProvider) (*workspace.PatchWorkspaceCommand, error) | ||
type PatchWorkspaceCommandHandlerFunc func(context.Context, workspace.PatchWorkspaceCommand) (*workspace.PatchWorkspaceResponse, error) | ||
|
||
// PatchWorkspaceHandler the http.Request handler for Patch Workspaces endpoint | ||
type PatchWorkspaceHandler struct { | ||
MapperFunc PatchWorkspaceMapperFunc | ||
CommandHandler PatchWorkspaceCommandHandlerFunc | ||
|
||
MarshalerProvider marshal.MarshalerProvider | ||
UnmarshalerProvider marshal.UnmarshalerProvider | ||
} | ||
|
||
// NewPatchWorkspaceHandler creates a PatchWorkspaceHandler | ||
func NewDefaultPatchWorkspaceHandler( | ||
handler PatchWorkspaceCommandHandlerFunc, | ||
) *PatchWorkspaceHandler { | ||
return NewPatchWorkspaceHandler( | ||
MapPatchWorkspaceHttp, | ||
handler, | ||
marshal.DefaultMarshalerProvider, | ||
marshal.DefaultUnmarshalerProvider, | ||
) | ||
} | ||
|
||
// NewPatchWorkspaceHandler creates a PatchWorkspaceHandler | ||
func NewPatchWorkspaceHandler( | ||
mapperFunc PatchWorkspaceMapperFunc, | ||
queryHandler PatchWorkspaceCommandHandlerFunc, | ||
marshalerProvider marshal.MarshalerProvider, | ||
unmarshalerProvider marshal.UnmarshalerProvider, | ||
) *PatchWorkspaceHandler { | ||
return &PatchWorkspaceHandler{ | ||
MapperFunc: mapperFunc, | ||
CommandHandler: queryHandler, | ||
MarshalerProvider: marshalerProvider, | ||
UnmarshalerProvider: unmarshalerProvider, | ||
} | ||
} | ||
|
||
func (h *PatchWorkspaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
l := log.FromContext(r.Context()) | ||
l.Debug("executing update") | ||
|
||
// build marshaler for the given request | ||
l.Debug("building marshaler for request") | ||
m, err := h.MarshalerProvider(r) | ||
if err != nil { | ||
l.Debug("error building marshaler for request", "error", err) | ||
w.WriteHeader(http.StatusBadRequest) | ||
return | ||
} | ||
|
||
// map | ||
l.Debug("mapping request to update command") | ||
c, err := h.MapperFunc(r, h.UnmarshalerProvider) | ||
if err != nil { | ||
l.Debug("error mapping request to command", "error", err) | ||
w.WriteHeader(http.StatusBadRequest) | ||
return | ||
} | ||
|
||
// execute | ||
l.Debug("executing update command", "command", c) | ||
cr, err := h.CommandHandler(r.Context(), *c) | ||
if err != nil { | ||
l = l.With("error", err) | ||
switch { | ||
case errors.Is(err, core.ErrNotFound): | ||
l.Debug("error executing update command: resource not found") | ||
w.WriteHeader(http.StatusNotFound) | ||
default: | ||
l.Error("error executing update command") | ||
w.WriteHeader(http.StatusInternalServerError) | ||
} | ||
return | ||
} | ||
|
||
// marshal response | ||
l.Debug("marshaling response", "response", &cr) | ||
d, err := m.Marshal(cr.Workspace) | ||
if err != nil { | ||
l.Error("unexpected error marshaling response", "error", err) | ||
w.WriteHeader(http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
// reply | ||
l.Debug("writing response", "response", d) | ||
w.Header().Add(header.ContentType, m.ContentType()) | ||
if _, err := w.Write(d); err != nil { | ||
l.Error("unexpected error writing response", "error", err) | ||
w.WriteHeader(http.StatusInternalServerError) | ||
return | ||
} | ||
} | ||
|
||
func MapPatchWorkspaceHttp(r *http.Request, provider marshal.UnmarshalerProvider) (*workspace.PatchWorkspaceCommand, error) { | ||
// parse request body | ||
d, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("error reading request body: %w", err) | ||
} | ||
|
||
ct, ok := r.Header["Content-Type"] | ||
if !ok || len(ct) != 1 { | ||
return nil, fmt.Errorf("Content-Type header is required") | ||
} | ||
|
||
// retrieve namespace from path | ||
n := r.PathValue("name") | ||
ns := r.PathValue("namespace") | ||
|
||
// build command | ||
return &workspace.PatchWorkspaceCommand{ | ||
Workspace: n, | ||
Owner: ns, | ||
PatchType: types.PatchType(ct[0]), | ||
Patch: d, | ||
}, nil | ||
} |