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

Commit 4bc71a0

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/v1alpha1 - discovery endpoint * /apis/resources.hnc.x-k8s.io/v1alpha1/{resource} - global resource query, equivalent to the same global request for the original resource * /apis/resources.hnc.x-k8s.io/v1alpha1/namespaces/{namespace}/{resource} - returns resources in all namespaces under {namespace}, including {namespace}.
1 parent 564b4a9 commit 4bc71a0

File tree

6 files changed

+822
-2
lines changed

6 files changed

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

0 commit comments

Comments
 (0)