diff --git a/main.go b/main.go index bc8f151..f69ced7 100644 --- a/main.go +++ b/main.go @@ -20,11 +20,8 @@ import ( "github.com/prometheus/client_golang/prometheus" flag "github.com/spf13/pflag" v1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/deprecated/scheme" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -59,7 +56,7 @@ func Main() error { listenInternal := flag.String("listen-internal", ":9090", "The address at which to to serve the internal API.") logLevel := flag.String("log-level", logLevelInfo, fmt.Sprintf("Log level to use. Possible values: %s", availableLogLevels)) apiServer := flag.String("server", "kubernetes", "The address of the Kubernetes API server to use in generated kubeconfigs.") - rolePath := flag.String("role", "", "The path to a file containing a Kubernetes RBAC role.") + clusterRole := flag.String("cluster-role", "", "The of a Kubernetes ClusterRole to bind to ServiceAccounts in created Namespaces.") prefix := flag.String("prefix", "np", "The prefix to use for Namespace names.") selector := flag.String("selector", "controller.observatorium.io=namespace-selector", "The label selector to use to select resources.") token := flag.String("token", "", "The token to require for authentication with the API.") @@ -99,16 +96,6 @@ func Main() error { prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}), ) - rawRole, err := os.ReadFile(*rolePath) - if err != nil { - return fmt.Errorf("failed to read Role %q: %w", *rolePath, err) - } - role := &rbacv1.Role{} - if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), rawRole, role); err != nil { - return fmt.Errorf("failed to decode Role: %w", err) - } - role.Name = np - ls, err := labels.ConvertSelectorToLabelsMap(*selector) if err != nil { return fmt.Errorf("failed to parse label selector: %w", err) @@ -148,7 +135,7 @@ func Main() error { if err != nil { return err } - h := newHander(l, r, c, factory, ls, apiServerURL, *prefix, role, *token, *ttl) + h := newHander(l, r, c, factory, ls, apiServerURL, *prefix, *clusterRole, *token, *ttl) s := http.Server{Addr: *listen, Handler: h} // Start the API server. diff --git a/manifests/grant.yaml b/manifests/grant.yaml new file mode 100644 index 0000000..8bc514b --- /dev/null +++ b/manifests/grant.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: namespace-provisioner-grant + labels: + app.kubernetes.io/name: namespace-provisioner-grant + app.kubernetes.io/part-of: namespace-provisioner +rules: [] diff --git a/manifests/namespace-provisioner.yaml b/manifests/namespace-provisioner.yaml new file mode 100644 index 0000000..8810de6 --- /dev/null +++ b/manifests/namespace-provisioner.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: namespace-provisioner + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner +--- +apiVersion: v1 +kind: Secret +metadata: + name: namespace-provisioner + namespace: namespace-provisioner + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner +stringData: + token: PASSWORD +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: namespace-provisioner + namespace: namespace-provisioner + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: namespace-provisioner + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner +rules: +- apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - delete + - list + - watch +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - namespace-provisioner + verbs: + - get +- apiGroups: + - "" + resources: + - secrets + verbs: + - create +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - bind + resourceNames: + - namespace-provisioner-grant +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: namespace-provisioner + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: namespace-provisioner +subjects: + - kind: ServiceAccount + namespace: namespace-provisioner + name: namespace-provisioner +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: namespace-provisioner + namespace: namespace-provisioner + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner + template: + metadata: + labels: + app.kubernetes.io/name: namespace-provisioner + app.kubernetes.io/part-of: namespace-provisioner + spec: + serviceAccountName: namespace-provisioner + securityContext: + runAsNonRoot: true + runAsUser: 1000 + containers: + - name: namespace-provisioner + image: quay.io/observatorium/namespace-provisioner + imagePullPolicy: IfNotPresent + args: + - --listen=:8080 + - --listen-internal=:9090 + - --cluster-role=namespace-provisioner-grant + - --token=$(TOKEN) + env: + - name: TOKEN + valueFrom: + secretKeyRef: + name: namespace-provisioner + key: token + ports: + - containerPort: 8080 + name: http + - containerPort: 9090 + name: metrics diff --git a/role.yaml b/role.yaml deleted file mode 100644 index a26f7ba..0000000 --- a/role.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: namespace-provisioner -rules: -- apiGroups: - - "" - resources: - - pods - verbs: - - get - - list - - watch - - create - - update - - delete diff --git a/server.go b/server.go index 3dcdd54..d5c5565 100644 --- a/server.go +++ b/server.go @@ -46,13 +46,13 @@ type handler struct { logger log.Logger apiServerURL *url.URL prefix string - role *rbacv1.Role + clusterRole string ttl time.Duration duration *prometheus.HistogramVec } -func newHander(l log.Logger, r prometheus.Registerer, c kubernetes.Interface, factory informers.SharedInformerFactory, ls map[string]string, apiServerURL *url.URL, prefix string, role *rbacv1.Role, token string, ttl time.Duration) http.Handler { +func newHander(l log.Logger, r prometheus.Registerer, c kubernetes.Interface, factory informers.SharedInformerFactory, ls map[string]string, apiServerURL *url.URL, prefix string, clusterRole string, token string, ttl time.Duration) http.Handler { if l == nil { l = log.NewNopLogger() } @@ -63,7 +63,7 @@ func newHander(l log.Logger, r prometheus.Registerer, c kubernetes.Interface, fa labels: ls, logger: l, apiServerURL: apiServerURL, - role: role, + clusterRole: clusterRole, prefix: prefix, ttl: ttl, @@ -148,14 +148,6 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { return } - role := h.role.DeepCopy() - role.Namespace = namespace - role.ObjectMeta.Labels = h.labels - if _, err := h.c.RbacV1().Roles(namespace).Create(r.Context(), role, metav1.CreateOptions{}); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - rb := &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: np, @@ -164,8 +156,8 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, - Kind: role.Kind, - Name: role.GetName(), + Kind: "ClusterRole", + Name: h.clusterRole, }, Subjects: []rbacv1.Subject{{ Kind: "ServiceAccount", @@ -180,14 +172,6 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } // Create Kubeconfig - - // TODO: Use an informer or something proper as this might race - famous last words - sa, err = h.c.CoreV1().ServiceAccounts(namespace).Get(r.Context(), sa.Name, metav1.GetOptions{}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - se, err = h.c.CoreV1().Secrets(namespace).Get(r.Context(), np, metav1.GetOptions{}) if err != nil { msg := "no secret for service account"