diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 30f7dd4..7e96cfe 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -32,6 +32,32 @@ type redirectRequest struct { Namespace string `json:"namespace,omitempty"` } +type redirectResponse struct { + From string `json:"from"` + To string `json:"to"` + CertificateReady bool `json:"certificateReady"` + Message string `json:"message,omitempty"` + CreatedAt string `json:"createdAt"` +} + +func toResponse(rd *decositesv1alpha1.RedirectDomain) redirectResponse { + resp := redirectResponse{ + From: rd.Spec.From, + To: rd.Spec.To, + CreatedAt: rd.CreationTimestamp.UTC().Format("2006-01-02T15:04:05Z"), + } + for _, c := range rd.Status.Conditions { + if c.Type == "CertificateReady" { + resp.CertificateReady = c.Status == "True" + if c.Status != "True" { + resp.Message = c.Message + } + break + } + } + return resp +} + func (h *Handlers) create(w http.ResponseWriter, r *http.Request) { var req redirectRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -72,15 +98,46 @@ func (h *Handlers) create(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) } +func (h *Handlers) get(w http.ResponseWriter, r *http.Request) { + rawDomain := strings.ToLower(strings.TrimSpace(r.PathValue("domain"))) + if !domainRe.MatchString(rawDomain) { + http.Error(w, "invalid domain", http.StatusBadRequest) + return + } + domain := domainToName(rawDomain) + ns := h.nsOrDefault(r.URL.Query().Get("namespace")) + + rd := &decositesv1alpha1.RedirectDomain{} + if err := h.client.Get(r.Context(), client.ObjectKey{Name: domain, Namespace: ns}, rd); err != nil { + status := http.StatusInternalServerError + if apierrors.IsNotFound(err) { + status = http.StatusNotFound + } + http.Error(w, err.Error(), status) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(toResponse(rd)) +} + func (h *Handlers) delete(w http.ResponseWriter, r *http.Request) { - domain := domainToName(strings.ToLower(strings.TrimSpace(r.PathValue("domain")))) + rawDomain := strings.ToLower(strings.TrimSpace(r.PathValue("domain"))) + if !domainRe.MatchString(rawDomain) { + http.Error(w, "invalid domain", http.StatusBadRequest) + return + } + domain := domainToName(rawDomain) ns := h.nsOrDefault(r.URL.Query().Get("namespace")) rd := &decositesv1alpha1.RedirectDomain{ ObjectMeta: metav1.ObjectMeta{Name: domain, Namespace: ns}, } if err := h.client.Delete(r.Context(), rd); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + status := http.StatusInternalServerError + if apierrors.IsNotFound(err) { + status = http.StatusNotFound + } + http.Error(w, err.Error(), status) return } w.WriteHeader(http.StatusNoContent) @@ -94,8 +151,12 @@ func (h *Handlers) list(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + items := make([]redirectResponse, len(list.Items)) + for i := range list.Items { + items[i] = toResponse(&list.Items[i]) + } w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(list.Items) + _ = json.NewEncoder(w).Encode(items) } // domainToName converts a domain to a valid k8s resource name (dots → dashes). diff --git a/internal/api/server.go b/internal/api/server.go index 93e4a9e..5ef5cdf 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -19,6 +19,7 @@ func NewServer(addr, user, pass string, h *Handlers) *Server { mux := http.NewServeMux() mux.HandleFunc("GET /redirects", h.list) mux.HandleFunc("POST /redirects", h.create) + mux.HandleFunc("GET /redirects/{domain}", h.get) mux.HandleFunc("DELETE /redirects/{domain}", h.delete) return &Server{ addr: addr, diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 08b2100..12fe781 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -69,7 +69,7 @@ func TestDelete_HappyPath(t *testing.T) { h := api.NewHandlers(fc, "deco-redirect-system") srv := api.NewServer(":0", "user", "pass", h) - req := httptest.NewRequest(http.MethodDelete, "/redirects/example-com", nil) + req := httptest.NewRequest(http.MethodDelete, "/redirects/example.com", nil) req.SetBasicAuth("user", "pass") rec := httptest.NewRecorder() srv.ServeHTTP(rec, req) @@ -78,6 +78,51 @@ func TestDelete_HappyPath(t *testing.T) { } } +func TestGet_HappyPath(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = decositesv1alpha1.AddToScheme(scheme) + fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&decositesv1alpha1.RedirectDomain{ + ObjectMeta: metav1.ObjectMeta{Name: "example-com", Namespace: "deco-redirect-system"}, + Spec: decositesv1alpha1.RedirectDomainSpec{From: "example.com", To: "https://www.example.com"}, + }).Build() + h := api.NewHandlers(fc, "deco-redirect-system") + srv := api.NewServer(":0", "user", "pass", h) + + req := httptest.NewRequest(http.MethodGet, "/redirects/example.com", nil) + req.SetBasicAuth("user", "pass") + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + var item struct { + From string `json:"from"` + To string `json:"to"` + } + _ = json.NewDecoder(rec.Body).Decode(&item) + if item.From != "example.com" { + t.Fatalf("expected from=example.com, got %s", item.From) + } +} + +func TestGet_NotFound(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = decositesv1alpha1.AddToScheme(scheme) + fc := fake.NewClientBuilder().WithScheme(scheme).Build() + h := api.NewHandlers(fc, "deco-redirect-system") + srv := api.NewServer(":0", "user", "pass", h) + + req := httptest.NewRequest(http.MethodGet, "/redirects/notfound.com", nil) + req.SetBasicAuth("user", "pass") + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rec.Code) + } +} + func TestList_HappyPath(t *testing.T) { scheme := runtime.NewScheme() _ = clientgoscheme.AddToScheme(scheme) @@ -96,7 +141,9 @@ func TestList_HappyPath(t *testing.T) { if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } - var items []decositesv1alpha1.RedirectDomain + var items []struct { + From string `json:"from"` + } _ = json.NewDecoder(rec.Body).Decode(&items) if len(items) != 1 { t.Fatalf("expected 1 item, got %d", len(items))