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

Commit c0da0c5

Browse files
authored
Merge pull request #281 from cmurphy/list-resources-apiextension
Add API to list resources under namespace tree
2 parents 50cd213 + 78037b5 commit c0da0c5

File tree

1,034 files changed

+124822
-46
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,034 files changed

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

config/apiextension/apiextension.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
apiVersion: apps/v1
3+
kind: Deployment
4+
metadata:
5+
labels:
6+
app: resourcelist-apiextension
7+
name: resourcelist-apiextension
8+
namespace: system
9+
spec:
10+
replicas: 1
11+
selector:
12+
matchLabels:
13+
app: resourcelist-apiextension
14+
template:
15+
metadata:
16+
labels:
17+
app: resourcelist-apiextension
18+
spec:
19+
securityContext:
20+
fsGroup: 2000
21+
runAsNonRoot: true
22+
runAsUser: 1000
23+
containers:
24+
- image: controller:latest # this is usually overridden by kustomize
25+
name: resourcelist
26+
command:
27+
- /apiextension
28+
args:
29+
- "--cert=/certs/tls.crt"
30+
- "--key=/certs/tls.key"
31+
imagePullPolicy: IfNotPresent
32+
volumeMounts:
33+
- name: certs
34+
mountPath: /certs
35+
securityContext:
36+
allowPrivilegeEscalation: false
37+
readOnlyRootFilesystem: true
38+
runAsNonRoot: true
39+
seccompProfile:
40+
type: RuntimeDefault
41+
capabilities:
42+
drop: ["ALL"]
43+
ports:
44+
- containerPort: 7443
45+
name: server
46+
protocol: TCP
47+
volumes:
48+
- secret:
49+
defaultMode: 420
50+
secretName: hnc-resourcelist-apiextension
51+
name: certs

config/apiextension/apiservice.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
apiVersion: apiregistration.k8s.io/v1
3+
kind: APIService
4+
metadata:
5+
name: v1alpha2.resources.hnc.x-k8s.io
6+
spec:
7+
group: resources.hnc.x-k8s.io
8+
version: v1alpha2
9+
groupPriorityMinimum: 10
10+
versionPriority: 10
11+
service:
12+
namespace: hnc-system
13+
name: resourcelist
14+
port: 7443
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
resources:
2+
- apiextension.yaml
3+
- service.yaml
4+
- apiservice.yaml

config/apiextension/service.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
apiVersion: v1
3+
kind: Service
4+
metadata:
5+
labels:
6+
app: resourcelist
7+
name: resourcelist
8+
spec:
9+
ports:
10+
- port: 7443
11+
protocol: TCP
12+
targetPort: 7443
13+
selector:
14+
app: resourcelist

config/certmanager/certificate.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,16 @@ spec:
2323
kind: Issuer
2424
name: selfsigned-issuer
2525
secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize
26+
---
27+
apiVersion: cert-manager.io/v1
28+
kind: Certificate
29+
metadata:
30+
name: resourcelist
31+
spec:
32+
dnsNames:
33+
- $(APIEXT_SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local
34+
- $(APIEXT_SERVICE_NAME).$(SERVICE_NAMESPACE).svc
35+
issuerRef:
36+
kind: Issuer
37+
name: selfsigned-issuer
38+
secretName: hnc-resourcelist

config/internalcert/manifests.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
---
12
apiVersion: v1
23
kind: Secret
34
metadata:
45
name: webhook-server-cert
56
namespace: system
7+
---
8+
apiVersion: v1
9+
kind: Secret
10+
metadata:
11+
name: resourcelist
12+
namespace: system

config/rbac/kustomization.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ resources:
66
- aggregate_to_admin.yaml
77
- leader_election_role.yaml
88
- leader_election_role_binding.yaml
9+
- resourcelist_role.yaml
10+
- resourcelist_rolebinding.yaml

config/rbac/resourcelist_role.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Allow the resourcelist-apiextension deployment to read resources in order to re-register them as HNC resources.
2+
---
3+
apiVersion: rbac.authorization.k8s.io/v1
4+
kind: ClusterRole
5+
metadata:
6+
name: resourcelist
7+
rules:
8+
- apiGroups:
9+
- "*"
10+
resources:
11+
- "*"
12+
verbs:
13+
- get
14+
- list
15+
- watch

0 commit comments

Comments
 (0)