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

Commit 15f6234

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 564b4a9 commit 15f6234

File tree

7 files changed

+1029
-2
lines changed

7 files changed

+1029
-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: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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/client-go/discovery"
16+
"k8s.io/client-go/dynamic"
17+
"k8s.io/client-go/dynamic/dynamicinformer"
18+
"k8s.io/client-go/informers"
19+
"k8s.io/client-go/kubernetes"
20+
corecache "k8s.io/client-go/listers/core/v1"
21+
"k8s.io/client-go/rest"
22+
"k8s.io/client-go/tools/cache"
23+
"k8s.io/client-go/tools/clientcmd"
24+
apiregv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
25+
"path/filepath"
26+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
27+
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
28+
"sigs.k8s.io/hierarchical-namespaces/internal/apiextension/apiresources"
29+
"sigs.k8s.io/hierarchical-namespaces/internal/apiextension/clients"
30+
"sigs.k8s.io/hierarchical-namespaces/internal/apiextension/handlers"
31+
)
32+
33+
const (
34+
kubeSystemNamespace = "kube-system"
35+
extensionConfigMap = "extension-apiserver-authentication"
36+
clientCAKey = "requestheader-client-ca-file"
37+
)
38+
39+
var (
40+
setupLog = zap.New().WithName("setup-apiext")
41+
listenAddress string
42+
certPath string
43+
keyPath string
44+
debug bool
45+
)
46+
47+
func main() {
48+
parseFlags()
49+
cfg, err := getConfig()
50+
if err != nil {
51+
setupLog.Error(err, "unable to get cluster config")
52+
os.Exit(1)
53+
}
54+
server(context.Background(), cfg)
55+
}
56+
57+
func parseFlags() {
58+
setupLog.Info("Parsing flags")
59+
flag.StringVar(&listenAddress, "address", ":7443", "The address to listen on.")
60+
flag.StringVar(&certPath, "cert", "", "Path to the server cert.")
61+
flag.StringVar(&keyPath, "key", "", "Path to the server key.")
62+
flag.BoolVar(&debug, "debug", false, "Enable debug logging.")
63+
flag.Parse()
64+
}
65+
66+
func getConfig() (*rest.Config, error) {
67+
cfg, err := rest.InClusterConfig()
68+
if err == nil {
69+
return cfg, nil
70+
}
71+
kubeconfig := os.Getenv("KUBECONFIG")
72+
if kubeconfig == "" {
73+
home, err := os.UserHomeDir()
74+
if err != nil {
75+
return nil, fmt.Errorf("could not get kubeconfig: %w", err)
76+
}
77+
kubeconfig = filepath.Join(home, ".kube", "config")
78+
}
79+
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
80+
if err != nil {
81+
return nil, err
82+
}
83+
return cfg, nil
84+
}
85+
86+
func server(ctx context.Context, cfg *rest.Config) {
87+
discovery, err := discovery.NewDiscoveryClientForConfig(cfg)
88+
if err != nil {
89+
setupLog.Error(err, "could not start watcher")
90+
os.Exit(1)
91+
}
92+
clientGetter := clients.TableClientGetter(cfg)
93+
dynamicFactory, err := getDynamicInformerFactory(cfg)
94+
if err != nil {
95+
setupLog.Error(err, "could not get dynammic client for config")
96+
os.Exit(1)
97+
}
98+
crdInformer, apiServiceInformer := setUpAPIInformers(dynamicFactory, ctx.Done())
99+
apis := apiresources.WatchAPIResources(ctx, discovery, crdInformer, apiServiceInformer)
100+
factory, err := getInformerFactory(cfg)
101+
if err != nil {
102+
setupLog.Error(err, "could not get informer factory")
103+
os.Exit(1)
104+
}
105+
namespaceCache, configMapCache, err := setUpInformers(factory, ctx.Done())
106+
if err != nil {
107+
setupLog.Error(err, "failed to set up informers")
108+
os.Exit(1)
109+
}
110+
mux := mux.NewRouter()
111+
pathPrefix := fmt.Sprintf("/apis/%s", api.ResourcesGroupVersion.String())
112+
mux.HandleFunc(pathPrefix, handlers.DiscoveryHandler(apis))
113+
mux.HandleFunc(fmt.Sprintf("%s/{resource}", pathPrefix), handlers.Forwarder(clientGetter, apis))
114+
mux.HandleFunc(fmt.Sprintf("%s/namespaces/{namespace}/{resource}", pathPrefix), handlers.NamespaceHandler(clientGetter, apis, namespaceCache))
115+
mux.Use(handlers.AuthenticateMiddleware(configMapCache, extensionConfigMap))
116+
117+
clientCA, err := getClientCA(configMapCache)
118+
if err != nil {
119+
setupLog.Error(err, "could not get client CA from configmap")
120+
os.Exit(1)
121+
}
122+
caCertPool := x509.NewCertPool()
123+
caCertPool.AppendCertsFromPEM([]byte(clientCA))
124+
tlsConfig := &tls.Config{
125+
ClientCAs: caCertPool,
126+
ClientAuth: tls.RequireAndVerifyClientCert,
127+
}
128+
server := http.Server{
129+
Addr: listenAddress,
130+
Handler: mux,
131+
TLSConfig: tlsConfig,
132+
}
133+
setupLog.Info(fmt.Sprintf("starting server on %s", listenAddress))
134+
err = server.ListenAndServeTLS(certPath, keyPath)
135+
if err != nil {
136+
setupLog.Error(err, "could not start server")
137+
os.Exit(1)
138+
}
139+
}
140+
141+
func getClientCA(configMapCache corecache.ConfigMapNamespaceLister) (string, error) {
142+
config, err := configMapCache.Get(extensionConfigMap)
143+
if err != nil {
144+
return "", err
145+
}
146+
clientCA, ok := config.Data[clientCAKey]
147+
if !ok {
148+
return "", fmt.Errorf("invalid extension config")
149+
}
150+
return string(clientCA), nil
151+
}
152+
153+
func getInformerFactory(cfg *rest.Config) (informers.SharedInformerFactory, error) {
154+
clientset, err := kubernetes.NewForConfig(cfg)
155+
if err != nil {
156+
return nil, err
157+
}
158+
return informers.NewSharedInformerFactory(clientset, 0), nil
159+
}
160+
161+
func getDynamicInformerFactory(cfg *rest.Config) (dynamicinformer.DynamicSharedInformerFactory, error) {
162+
dynamicClient, err := dynamic.NewForConfig(cfg)
163+
if err != nil {
164+
return nil, err
165+
}
166+
return dynamicinformer.NewDynamicSharedInformerFactory(dynamicClient, time.Minute), nil
167+
}
168+
169+
func setUpInformers(factory informers.SharedInformerFactory, stop <-chan struct{}) (corecache.NamespaceLister, corecache.ConfigMapNamespaceLister, error) {
170+
namespaceInformer := factory.Core().V1().Namespaces()
171+
configMapInformer := factory.Core().V1().ConfigMaps()
172+
go factory.Start(stop)
173+
if !cache.WaitForCacheSync(stop, namespaceInformer.Informer().HasSynced, configMapInformer.Informer().HasSynced) {
174+
return nil, nil, fmt.Errorf("cached failed to sync")
175+
}
176+
return namespaceInformer.Lister(), configMapInformer.Lister().ConfigMaps(kubeSystemNamespace), nil
177+
}
178+
179+
func setUpAPIInformers(factory dynamicinformer.DynamicSharedInformerFactory, stop <-chan struct{}) (cache.SharedIndexInformer, cache.SharedIndexInformer) {
180+
crdGVR := apiextv1.SchemeGroupVersion.WithResource("customresourcedefinitions")
181+
crdInformer := factory.ForResource(crdGVR).Informer()
182+
apiServiceGVR := apiregv1.SchemeGroupVersion.WithResource("apiservices")
183+
apiServiceInformer := factory.ForResource(apiServiceGVR).Informer()
184+
go factory.Start(stop)
185+
return crdInformer, apiServiceInformer
186+
}

0 commit comments

Comments
 (0)