Skip to content
This repository was archived by the owner on Apr 17, 2025. It is now read-only.

Commit 7c4e376

Browse files
committed
Add API to list resources under namespace tree
This change adds a new command and configuration to register an API endpoint which will allow users to query for resources belonging to descendants of any namespace. This means that finding all resources under a parent namespace can be done in one request, instead finding and iterating over all descendants. The new endpoints are: * /apis/resources.hnc.x-k8s.io/v1alpha2 - discovery endpoint * /apis/resources.hnc.x-k8s.io/v1alpha2/{resource} - global resource query, equivalent to the same global request for the original resource * /apis/resources.hnc.x-k8s.io/v1alpha2/namespaces/{namespace}/{resource} - returns resources in all namespaces under {namespace}, including {namespace}.
1 parent 74fe014 commit 7c4e376

File tree

9 files changed

+2140
-2
lines changed

9 files changed

+2140
-2
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ COPY internal/ internal/
1414

1515
# Build
1616
RUN CGO_ENABLED=0 GO111MODULE=on go build -a -o manager ./cmd/manager/main.go
17+
RUN CGO_ENABLED=0 GO111MODULE=on go build -a -o apiextension ./cmd/apiextension/main.go
1718

1819
# Copied from kubebuilder scaffold to run as nonroot at
1920
# https://github.com/kubernetes-sigs/kubebuilder/blob/7af89cb00c224c57ece37dc14ea37caf1eb769db/pkg/scaffold/v2/dockerfile.go#L60
@@ -22,5 +23,6 @@ RUN CGO_ENABLED=0 GO111MODULE=on go build -a -o manager ./cmd/manager/main.go
2223
FROM gcr.io/distroless/static:nonroot
2324
WORKDIR /
2425
COPY --from=builder /workspace/manager .
26+
COPY --from=builder /workspace/apiextension .
2527
USER nonroot:nonroot
2628
ENTRYPOINT ["/manager"]

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ test-only: build-setup-envtest
113113
# Builds all binaries (manager and kubectl) and manifests
114114
build: generate fmt vet staticcheck manifests
115115
go build -o bin/manager ./cmd/manager/main.go
116+
go build -o bin/apiextension ./cmd/apiextension/main.go
116117
GOOS=linux GOARCH=amd64 go build \
117118
-o bin/kubectl/kubectl-hns_linux_amd64 \
118119
-ldflags="-X sigs.k8s.io/hierarchical-namespaces/internal/version.Version=${HNC_IMG_TAG}" \

api/v1alpha2/groupversion_info.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ import (
2424
)
2525

2626
var (
27-
// GroupVersion is group version used to register these objects
27+
// GroupVersion is group version used to register the main HNC objects.
2828
GroupVersion = schema.GroupVersion{Group: "hnc.x-k8s.io", Version: "v1alpha2"}
2929

30-
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
30+
// ResourcesGroupVersion is the group used to register resources in an HNC tree.
31+
ResourcesGroupVersion = schema.GroupVersion{Group: "resources.hnc.x-k8s.io", Version: "v1alpha2"}
32+
33+
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
3134
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
3235

3336
// AddToScheme adds the types in this group-version to the given scheme.

cmd/apiextension/main.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"flag"
8+
"fmt"
9+
"net/http"
10+
"os"
11+
"time"
12+
13+
"github.com/gorilla/mux"
14+
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
15+
"k8s.io/apimachinery/pkg/api/meta"
16+
"k8s.io/client-go/discovery"
17+
"k8s.io/client-go/discovery/cached/memory"
18+
"k8s.io/client-go/dynamic"
19+
"k8s.io/client-go/dynamic/dynamicinformer"
20+
"k8s.io/client-go/informers"
21+
"k8s.io/client-go/kubernetes"
22+
corecache "k8s.io/client-go/listers/core/v1"
23+
"k8s.io/client-go/rest"
24+
"k8s.io/client-go/restmapper"
25+
"k8s.io/client-go/tools/cache"
26+
"k8s.io/client-go/tools/clientcmd"
27+
apiregv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
28+
"path/filepath"
29+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
30+
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
31+
"sigs.k8s.io/hierarchical-namespaces/internal/apiextension/apiresources"
32+
"sigs.k8s.io/hierarchical-namespaces/internal/apiextension/clients"
33+
"sigs.k8s.io/hierarchical-namespaces/internal/apiextension/handlers"
34+
)
35+
36+
const (
37+
kubeSystemNamespace = "kube-system"
38+
extensionConfigMap = "extension-apiserver-authentication"
39+
clientCAKey = "requestheader-client-ca-file"
40+
)
41+
42+
var (
43+
setupLog = zap.New().WithName("setup-apiext")
44+
listenAddress string
45+
certPath string
46+
keyPath string
47+
debug bool
48+
)
49+
50+
func main() {
51+
parseFlags()
52+
cfg, err := getConfig()
53+
if err != nil {
54+
setupLog.Error(err, "unable to get cluster config")
55+
os.Exit(1)
56+
}
57+
server(context.Background(), cfg)
58+
}
59+
60+
func parseFlags() {
61+
setupLog.Info("Parsing flags")
62+
flag.StringVar(&listenAddress, "address", ":7443", "The address to listen on.")
63+
flag.StringVar(&certPath, "cert", "", "Path to the server cert.")
64+
flag.StringVar(&keyPath, "key", "", "Path to the server key.")
65+
flag.BoolVar(&debug, "debug", false, "Enable debug logging.")
66+
flag.Parse()
67+
}
68+
69+
func getConfig() (*rest.Config, error) {
70+
cfg, err := rest.InClusterConfig()
71+
if err == nil {
72+
return cfg, nil
73+
}
74+
kubeconfig := os.Getenv("KUBECONFIG")
75+
if kubeconfig == "" {
76+
home, err := os.UserHomeDir()
77+
if err != nil {
78+
return nil, fmt.Errorf("could not get kubeconfig: %w", err)
79+
}
80+
kubeconfig = filepath.Join(home, ".kube", "config")
81+
}
82+
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
83+
if err != nil {
84+
return nil, err
85+
}
86+
return cfg, nil
87+
}
88+
89+
func getMapper(cfg *rest.Config) (meta.RESTMapper, error) {
90+
k8sClient, err := kubernetes.NewForConfig(cfg)
91+
if err != nil {
92+
return nil, err
93+
}
94+
// rest mapper setup inspired by https://github.com/rancher/wrangler/blob/3032665ca5611788334c8e49516014278160ebe2/pkg/clients/clients.go#L134-L135
95+
// which is in turn borrowed from https://ymmt2005.hatenablog.com/entry/2020/04/14/An_example_of_using_dynamic_client_of_k8s.io/client-go
96+
cache := memory.NewMemCacheClient(k8sClient.Discovery())
97+
return restmapper.NewDeferredDiscoveryRESTMapper(cache), nil
98+
}
99+
100+
func server(ctx context.Context, cfg *rest.Config) {
101+
discovery, err := discovery.NewDiscoveryClientForConfig(cfg)
102+
if err != nil {
103+
setupLog.Error(err, "could not start watcher")
104+
os.Exit(1)
105+
}
106+
clientGetter := clients.MediaTypeClientGetter(cfg)
107+
dynamicFactory, err := getDynamicInformerFactory(cfg)
108+
if err != nil {
109+
setupLog.Error(err, "could not get dynammic client for config")
110+
os.Exit(1)
111+
}
112+
crdInformer, apiServiceInformer := setUpAPIInformers(dynamicFactory, ctx.Done())
113+
mapper, err := getMapper(cfg)
114+
if err != nil {
115+
setupLog.Error(err, "could not get REST mapper for config")
116+
os.Exit(1)
117+
}
118+
apis := apiresources.WatchAPIResources(ctx, discovery, crdInformer, apiServiceInformer, mapper)
119+
factory, err := getInformerFactory(cfg)
120+
if err != nil {
121+
setupLog.Error(err, "could not get informer factory")
122+
os.Exit(1)
123+
}
124+
namespaceCache, configMapCache, err := setUpInformers(factory, ctx.Done())
125+
if err != nil {
126+
setupLog.Error(err, "failed to set up informers")
127+
os.Exit(1)
128+
}
129+
mux := mux.NewRouter()
130+
pathPrefix := fmt.Sprintf("/apis/%s", api.ResourcesGroupVersion.String())
131+
mux.HandleFunc(pathPrefix, handlers.DiscoveryHandler(apis))
132+
mux.HandleFunc(fmt.Sprintf("%s/{resource}", pathPrefix), handlers.Forwarder(clientGetter, apis))
133+
mux.HandleFunc(fmt.Sprintf("%s/namespaces/{namespace}/{resource}", pathPrefix), handlers.NamespaceHandler(clientGetter, apis, namespaceCache))
134+
mux.Use(handlers.AuthenticateMiddleware(configMapCache, extensionConfigMap))
135+
136+
clientCA, err := getClientCA(configMapCache)
137+
if err != nil {
138+
setupLog.Error(err, "could not get client CA from configmap")
139+
os.Exit(1)
140+
}
141+
caCertPool := x509.NewCertPool()
142+
caCertPool.AppendCertsFromPEM([]byte(clientCA))
143+
tlsConfig := &tls.Config{
144+
ClientCAs: caCertPool,
145+
ClientAuth: tls.RequireAndVerifyClientCert,
146+
}
147+
server := http.Server{
148+
Addr: listenAddress,
149+
Handler: mux,
150+
TLSConfig: tlsConfig,
151+
}
152+
setupLog.Info(fmt.Sprintf("starting server on %s", listenAddress))
153+
err = server.ListenAndServeTLS(certPath, keyPath)
154+
if err != nil {
155+
setupLog.Error(err, "could not start server")
156+
os.Exit(1)
157+
}
158+
}
159+
160+
func getClientCA(configMapCache corecache.ConfigMapNamespaceLister) (string, error) {
161+
config, err := configMapCache.Get(extensionConfigMap)
162+
if err != nil {
163+
return "", err
164+
}
165+
clientCA, ok := config.Data[clientCAKey]
166+
if !ok {
167+
return "", fmt.Errorf("invalid extension config")
168+
}
169+
return string(clientCA), nil
170+
}
171+
172+
func getInformerFactory(cfg *rest.Config) (informers.SharedInformerFactory, error) {
173+
clientset, err := kubernetes.NewForConfig(cfg)
174+
if err != nil {
175+
return nil, err
176+
}
177+
return informers.NewSharedInformerFactory(clientset, 0), nil
178+
}
179+
180+
func getDynamicInformerFactory(cfg *rest.Config) (dynamicinformer.DynamicSharedInformerFactory, error) {
181+
dynamicClient, err := dynamic.NewForConfig(cfg)
182+
if err != nil {
183+
return nil, err
184+
}
185+
return dynamicinformer.NewDynamicSharedInformerFactory(dynamicClient, time.Minute), nil
186+
}
187+
188+
// setUpInformers returns listers (i.e. cache interfaces) for namespaces and for configmaps in the kube-system namespace.
189+
// See https://medium.com/codex/explore-client-go-informer-patterns-4415bb5f1fbd for information on the informer factory pattern.
190+
func setUpInformers(factory informers.SharedInformerFactory, stop <-chan struct{}) (corecache.NamespaceLister, corecache.ConfigMapNamespaceLister, error) {
191+
namespaceInformer := factory.Core().V1().Namespaces()
192+
configMapInformer := factory.Core().V1().ConfigMaps()
193+
go factory.Start(stop)
194+
if !cache.WaitForCacheSync(stop, namespaceInformer.Informer().HasSynced, configMapInformer.Informer().HasSynced) {
195+
return nil, nil, fmt.Errorf("cached failed to sync")
196+
}
197+
return namespaceInformer.Lister(), configMapInformer.Lister().ConfigMaps(kubeSystemNamespace), nil
198+
}
199+
200+
// setUpAPIInformers returns informer objects for CRDs and APIServices, which are used for discoverying resources.
201+
// These informer objects can be used later to add event handlers for when CRDs or APIServies are added, modified, or removed.
202+
func setUpAPIInformers(factory dynamicinformer.DynamicSharedInformerFactory, stop <-chan struct{}) (cache.SharedIndexInformer, cache.SharedIndexInformer) {
203+
crdGVR := apiextv1.SchemeGroupVersion.WithResource("customresourcedefinitions")
204+
crdInformer := factory.ForResource(crdGVR).Informer()
205+
apiServiceGVR := apiregv1.SchemeGroupVersion.WithResource("apiservices")
206+
apiServiceInformer := factory.ForResource(apiServiceGVR).Informer()
207+
go factory.Start(stop)
208+
return crdInformer, apiServiceInformer
209+
}

0 commit comments

Comments
 (0)