diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2442de7..18b9832 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,8 +62,10 @@ jobs: run: | kubectl -n namespace-provisioner port-forward service/namespace-provisioner 8080 & until lsof -nP -iTCP:8080 -sTCP:LISTEN >/dev/null; do sleep 1; done - curl localhost:8080/api/v1/namespace -X POST -H "Authorization: bearer PASSWORD" > kubeconfig + curl localhost:8080/api/v1/namespace?ttl=1s -X POST -H "Authorization: bearer PASSWORD" > kubeconfig kubectl --kubeconfig kubeconfig get pods + sleep 1 + [ $(kubectl get ns | grep np- | wc -l) -eq 0 ] - name: Debug failure if: failure() run: | diff --git a/README.md b/README.md index 13e9dc4..968ae8c 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ The Namespace Provisioner runs an API server over HTTP that exposes two API endp #### Namespace Creation - POST /api/v1/namespace -The Namespace creation endpoint accepts the following parameters: -1. ttl: the time in seconds that the Namespace should exist in the Kubernetes cluster; if 0 is given, then the Namespace Provisioner’s default lifetime is applied. +The Namespace creation endpoint accepts the following optional query parameters: +1. `ttl`: the time in seconds that the Namespace should exist in the Kubernetes cluster; if 0 is given, then the Namespace Provisioner’s default lifetime is applied. All provisioned Namespaces will be labeled with a Unix timestamp equal to the current time plus this duration; and -1. Optional: Kubernetes API URL; the endpoint of the Kubernetes API that the generated Kubeconfig should use. +1. `url`; the URL of the Kubernetes API that the generated Kubeconfig should use. The Namespace creation endpoint responds with the following data: 1. A Kubeconfig with scoped privileges for the provisioned Namespace using the provided RBAC Role and the Kubernetes API URL provided in the creation request. diff --git a/server.go b/server.go index d6675a4..455c7c9 100644 --- a/server.go +++ b/server.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -95,6 +96,19 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { h.duration.WithLabelValues("create").Observe(time.Since(start).Seconds()) }(start) + ttl := h.ttl + if r.URL.Query().Has("ttl") { + s, err := strconv.Atoi(r.URL.Query().Get("ttl")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + ttl = time.Duration(s) * time.Second + if ttl == 0 || (h.ttl > 0 && ttl > h.ttl) { + ttl = h.ttl + } + } + namespace := fmt.Sprintf("%s-%s", h.prefix, uuid.Must(uuid.NewUUID()).String()) ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ @@ -107,17 +121,19 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { return } - // Schedule asynchronous deletion of the namespace. - go func() { - <-time.After(h.ttl) - dpf := metav1.DeletePropagationForeground - if err := h.c.CoreV1().Namespaces().Delete(r.Context(), namespace, metav1.DeleteOptions{PropagationPolicy: &dpf}); err != nil { - if errors.IsNotFound(err) { - return + if ttl != 0 { + // Schedule asynchronous deletion of the namespace. + go func() { + <-time.After(ttl) + dpf := metav1.DeletePropagationForeground + if err := h.c.CoreV1().Namespaces().Delete(r.Context(), namespace, metav1.DeleteOptions{PropagationPolicy: &dpf}); err != nil { + if errors.IsNotFound(err) { + return + } + level.Error(h.logger).Log("msg", "failed to clean up namespace", "err", err) } - level.Error(h.logger).Log("msg", "failed to clean up namespace", "err", err) - } - }() + }() + } sa := &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{