From 5438f60cdf7c6dce10f7c05d5f96321754b846c5 Mon Sep 17 00:00:00 2001 From: PuneetPunamiya Date: Tue, 22 Aug 2023 19:11:39 +0530 Subject: [PATCH] Adds implementation and test for API: `/approvaltask/{approvalTaskName}` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This patch adds an API with POST method to update the approval task status - The request body will have { "namespace": “”, "approved": “true/false”, }, and will return name and the status of the approval task Signed-off-by: Puneet Punamiya ppunamiy@redhat.com --- cmd/approver/main.go | 4 + pkg/handlers/app/app.go | 6 +- pkg/handlers/update.go | 102 ++++++++++++++++++++ pkg/handlers/update_test.go | 183 ++++++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 pkg/handlers/update.go create mode 100644 pkg/handlers/update_test.go diff --git a/cmd/approver/main.go b/cmd/approver/main.go index 64db4e72..83b340c6 100644 --- a/cmd/approver/main.go +++ b/cmd/approver/main.go @@ -53,6 +53,10 @@ func main() { handlers.ListApprovalTask(w, r, dynamicClient) }) + r.Post("/approvaltask/{approvalTaskName}", func(w http.ResponseWriter, r *http.Request) { + handlers.UpdateApprovalTask(w, r, dynamicClient) + }) + // Bind to a port and pass our router in log.Fatal(http.ListenAndServe(":8000", r)) } diff --git a/pkg/handlers/app/app.go b/pkg/handlers/app/app.go index c6076b1f..c2d60c1e 100644 --- a/pkg/handlers/app/app.go +++ b/pkg/handlers/app/app.go @@ -34,10 +34,14 @@ var ( type ApprovalTask struct { Name string `json:"name"` - Namespace string `json:"namespace"` + Namespace string `json:"namespace,omitempty"` Approved bool `json:"approved"` } type ApprovalTaskList struct { Data []ApprovalTask `json:"data"` } + +type ApprovalTaskResult struct { + Data ApprovalTask `json:"data"` +} diff --git a/pkg/handlers/update.go b/pkg/handlers/update.go new file mode 100644 index 00000000..0622a411 --- /dev/null +++ b/pkg/handlers/update.go @@ -0,0 +1,102 @@ +/* +Copyright 2023 The OpenShift Pipelines Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handlers + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/openshift-pipelines/manual-approval-gate/pkg/apis/approvaltask/v1alpha1" + "github.com/openshift-pipelines/manual-approval-gate/pkg/handlers/app" + kErr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" +) + +func UpdateApprovalTask(res http.ResponseWriter, req *http.Request, dynamicClient dynamic.Interface) { + // Get the approvalTask Name from the url + approvalTaskName := chi.URLParam(req, "approvalTaskName") + + var requestBody app.ApprovalTask + decoder := json.NewDecoder(req.Body) + err := decoder.Decode(&requestBody) + if err != nil { + http.Error(res, "Invalid request body", http.StatusBadRequest) + return + } + + // Fetch the resource requested by the user + customResource, err := dynamicClient.Resource(app.CustomResourceGVR).Namespace(requestBody.Namespace).Get(context.TODO(), approvalTaskName, metav1.GetOptions{}) + if err != nil { + if kErr.IsNotFound(err) { + http.Error(res, "No resource found", http.StatusNotFound) + return + } else { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + } + + patchData := map[string]interface{}{ + "spec": map[string]interface{}{ + "approved": requestBody.Approved, + }, + } + + patch, err := json.Marshal(patchData) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + // Patch the approvalTask using the data from the request body + _, err = dynamicClient.Resource(app.CustomResourceGVR).Namespace(customResource.GetNamespace()).Patch(context.TODO(), + customResource.GetName(), + types.MergePatchType, + patch, + metav1.PatchOptions{}, + ) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + at := &v1alpha1.ApprovalTask{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(customResource.Object, at) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + at.Spec.Approved = requestBody.Approved + + var approvalTaskStatus = &app.ApprovalTaskResult{ + Data: app.ApprovalTask{ + Name: at.Name, + Approved: at.Spec.Approved, + }, + } + + res.WriteHeader(http.StatusOK) + if err := json.NewEncoder(res).Encode(approvalTaskStatus); err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/pkg/handlers/update_test.go b/pkg/handlers/update_test.go new file mode 100644 index 00000000..eab6806a --- /dev/null +++ b/pkg/handlers/update_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2023 The OpenShift Pipelines Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handlers + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/openshift-pipelines/manual-approval-gate/pkg/handlers/app" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic/fake" +) + +func TestUpdateApprovalTask(t *testing.T) { + scheme := runtime.NewScheme() + + scheme.AddKnownTypeWithName(schema.GroupVersionKind{ + Group: "openshift-pipelines.org", + Version: "v1alpha1", + Kind: "ApprovalTask", + }, &unstructured.Unstructured{}) + + // Create a fake client with the registered scheme and custom list kinds + fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, map[schema.GroupVersionResource]string{ + app.CustomResourceGVR: "ApprovalTaskList", + }) + + // Create a fake custom resource and add it to the fake client. + fakeApprovalTask := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "openshift-pipelines.org/v1alpha1", + "kind": "ApprovalTask", + "metadata": map[string]interface{}{ + "name": "example-task", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "approved": false, + }, + }, + } + _, err := fakeClient.Resource(schema.GroupVersionResource{ + Group: "openshift-pipelines.org", + Version: "v1alpha1", + Resource: "approvaltasks", + }).Namespace("default").Create(context.TODO(), fakeApprovalTask, metav1.CreateOptions{}) + assert.NoError(t, err, "Error creating fakeApprovalTask") + + r := chi.NewRouter() + r.Post("/approvaltask/{approvalTaskName}", func(w http.ResponseWriter, request *http.Request) { + UpdateApprovalTask(w, request, fakeClient) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + data := `{"approved":true, "namespace":"default"}` + resp, err := http.Post(ts.URL+"/approvaltask/example-task", "application/json", strings.NewReader(data)) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode, "Expected HTTP status OK") + + var approvalTask *app.ApprovalTaskResult + err = json.NewDecoder(resp.Body).Decode(&approvalTask) + assert.NoError(t, err) + + assert.Equal(t, true, approvalTask.Data.Approved) +} + +func TestUpdateApprovalTaskNotFound(t *testing.T) { + scheme := runtime.NewScheme() + + scheme.AddKnownTypeWithName(schema.GroupVersionKind{ + Group: "openshift-pipelines.org", + Version: "v1alpha1", + Kind: "ApprovalTask", + }, &unstructured.Unstructured{}) + + // Create a fake client with the registered scheme and custom list kinds + fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, map[schema.GroupVersionResource]string{ + app.CustomResourceGVR: "ApprovalTaskList", + }) + + r := chi.NewRouter() + r.Post("/approvaltask/{approvalTaskName}", func(w http.ResponseWriter, request *http.Request) { + UpdateApprovalTask(w, request, fakeClient) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + data := `{"approved":true, "namespace":"default"}` + resp, err := http.Post(ts.URL+"/approvaltask/example-task", "application/json", strings.NewReader(data)) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Expected HTTP Not Found Error") + + bodyBytes, _ := io.ReadAll(resp.Body) + assert.Equal(t, "No resource found\n", string(bodyBytes)) +} + +func TestUpdateApprovalTaskNotFoundInNamespace(t *testing.T) { + scheme := runtime.NewScheme() + + scheme.AddKnownTypeWithName(schema.GroupVersionKind{ + Group: "openshift-pipelines.org", + Version: "v1alpha1", + Kind: "ApprovalTask", + }, &unstructured.Unstructured{}) + + // Create a fake client with the registered scheme and custom list kinds + fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, map[schema.GroupVersionResource]string{ + app.CustomResourceGVR: "ApprovalTaskList", + }) + + // Create a fake custom resource and add it to the fake client. + fakeApprovalTask := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "openshift-pipelines.org/v1alpha1", + "kind": "ApprovalTask", + "metadata": map[string]interface{}{ + "name": "example-task", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "approved": false, + }, + }, + } + _, err := fakeClient.Resource(schema.GroupVersionResource{ + Group: "openshift-pipelines.org", + Version: "v1alpha1", + Resource: "approvaltasks", + }).Namespace("default").Create(context.TODO(), fakeApprovalTask, metav1.CreateOptions{}) + assert.NoError(t, err, "Error creating fakeApprovalTask") + + r := chi.NewRouter() + r.Post("/approvaltask/{approvalTaskName}", func(w http.ResponseWriter, request *http.Request) { + UpdateApprovalTask(w, request, fakeClient) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + data := `{"approved":true, "namespace":"test"}` + resp, err := http.Post(ts.URL+"/approvaltask/example-task", "application/json", strings.NewReader(data)) + assert.NoError(t, err) + + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Expected HTTP Internal Error") + + bodyBytes, _ := io.ReadAll(resp.Body) + assert.Equal(t, "No resource found\n", string(bodyBytes)) +}