Skip to content

Commit 14d793a

Browse files
authored
Merge pull request #3199 from mjudeikis/mjudeikis/vw.dev
✨ Enhance local-dev for VirtualWorkspaces
2 parents 7bf8cd5 + 35f051d commit 14d793a

File tree

10 files changed

+330
-140
lines changed

10 files changed

+330
-140
lines changed

cmd/kcp/kcp.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,26 @@ func main() {
6868

6969
// manually extract root directory from flags first as it influence all other flags
7070
rootDir := ".kcp"
71+
additionalMappingsFile := ""
7172
for i, f := range os.Args {
7273
if f == "--root-directory" {
7374
if i < len(os.Args)-1 {
7475
rootDir = os.Args[i+1]
7576
} // else let normal flag processing fail
7677
} else if strings.HasPrefix(f, "--root-directory=") {
7778
rootDir = strings.TrimPrefix(f, "--root-directory=")
79+
} else if f == "--miniproxy-mapping-file" {
80+
if i < len(os.Args)-1 {
81+
additionalMappingsFile = os.Args[i+1]
82+
} // else let normal flag processing fail
83+
} else if strings.HasPrefix(f, "--miniproxy-mapping-file") {
84+
additionalMappingsFile = strings.TrimPrefix(f, "--mapping-file=")
7885
}
7986
}
8087

8188
kcpOptions := options.NewOptions(rootDir)
8289
kcpOptions.Server.GenericControlPlane.Logs.Verbosity = logsapiv1.VerbosityLevel(2)
90+
kcpOptions.Server.Extra.AdditionalMappingsFile = additionalMappingsFile
8391

8492
startCmd := &cobra.Command{
8593
Use: "start",

cmd/kcp/options/generic.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@ import (
2727

2828
type GenericOptions struct {
2929
RootDirectory string
30+
MappingFile string
3031
}
3132

3233
func NewGeneric(rootDir string) *GenericOptions {
3334
return &GenericOptions{
3435
RootDirectory: rootDir,
36+
MappingFile: "",
3537
}
3638
}
3739

3840
func (o *GenericOptions) AddFlags(fss *cliflag.NamedFlagSets) {
3941
fs := fss.FlagSet("KCP")
4042
fs.StringVar(&o.RootDirectory, "root-directory", o.RootDirectory, "Root directory. Set to \"\" to disable file (e.g. certificates) generation in a root directory.")
43+
fs.StringVar(&o.MappingFile, "miniproxy-mapping-file", o.MappingFile, "DEVELOPMENT ONLY. Path to additional mapping file to be used by mini-front-proxy. This should not be used in production. For production usecase use front-proxy component instead.")
4144
}
4245

4346
func (o *GenericOptions) Complete() (*GenericOptions, error) {
@@ -55,6 +58,15 @@ func (o *GenericOptions) Complete() (*GenericOptions, error) {
5558
return nil, err
5659
}
5760
}
61+
if o.MappingFile != "" {
62+
if !filepath.IsAbs(o.MappingFile) {
63+
pwd, err := os.Getwd()
64+
if err != nil {
65+
return nil, err
66+
}
67+
o.MappingFile = filepath.Join(pwd, o.MappingFile)
68+
}
69+
}
5870

5971
return o, nil
6072
}

pkg/proxy/handler.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,17 @@ func shardHandler(index index.Index, proxy http.Handler) http.HandlerFunc {
5656
return
5757
}
5858

59-
shardURLString, found, errCode := index.LookupURL(clusterPath)
60-
if errCode != 0 {
61-
http.Error(w, "Not available.", errCode)
59+
result, found := index.LookupURL(clusterPath)
60+
if result.ErrorCode != 0 {
61+
http.Error(w, "Not available.", result.ErrorCode)
6262
return
6363
}
6464
if !found {
6565
logger.WithValues("clusterPath", clusterPath).V(4).Info("Unknown cluster path")
6666
responsewriters.Forbidden(req.Context(), attributes, w, req, kcpauthorization.WorkspaceAccessNotPermittedReason, kubernetesscheme.Codecs)
6767
return
6868
}
69-
shardURL, err := url.Parse(shardURLString)
69+
shardURL, err := url.Parse(result.URL)
7070
if err != nil {
7171
responsewriters.InternalError(w, req, err)
7272
return

pkg/proxy/index/index_controller.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const (
4949
)
5050

5151
type Index interface {
52-
LookupURL(path logicalcluster.Path) (url string, found bool, errorCode int)
52+
LookupURL(path logicalcluster.Path) (index.Result, bool)
5353
}
5454

5555
type ClusterClientGetter func(shard *corev1alpha1.Shard) (kcpclientset.ClusterInterface, error)
@@ -291,10 +291,16 @@ func (c *Controller) stopShard(shardName string) {
291291
delete(c.shardLogicalClusterInformers, shardName)
292292
}
293293

294-
func (c *Controller) LookupURL(path logicalcluster.Path) (url string, found bool, errorCode int) {
294+
func (c *Controller) LookupURL(path logicalcluster.Path) (index.Result, bool) {
295295
r, found := c.state.LookupURL(path)
296296
if found && r.ErrorCode != 0 {
297-
return r.URL, found, r.ErrorCode
297+
return index.Result{
298+
URL: r.URL,
299+
ErrorCode: r.ErrorCode,
300+
}, found
298301
}
299-
return r.URL, found, 0
302+
return index.Result{
303+
URL: r.URL,
304+
ErrorCode: 0,
305+
}, found
300306
}

pkg/proxy/mapping.go

Lines changed: 14 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -23,121 +23,34 @@ import (
2323
"net/http/httputil"
2424
"net/url"
2525
"os"
26-
"path"
27-
"strings"
28-
29-
"github.com/kcp-dev/logicalcluster/v3"
3026

3127
"k8s.io/component-base/metrics/legacyregistry"
3228
"k8s.io/klog/v2"
3329
"sigs.k8s.io/yaml"
3430

3531
"github.com/kcp-dev/kcp/pkg/proxy/index"
3632
proxyoptions "github.com/kcp-dev/kcp/pkg/proxy/options"
33+
"github.com/kcp-dev/kcp/pkg/server/proxy"
3734
)
3835

39-
// PathMapping describes how to route traffic from a path to a backend server.
40-
// Each Path is registered with the DefaultServeMux with a handler that
41-
// delegates to the specified backend.
42-
type PathMapping struct {
43-
Path string `json:"path"`
44-
Backend string `json:"backend"`
45-
BackendServerCA string `json:"backend_server_ca"`
46-
ProxyClientCert string `json:"proxy_client_cert"`
47-
ProxyClientKey string `json:"proxy_client_key"`
48-
UserHeader string `json:"user_header,omitempty"`
49-
GroupHeader string `json:"group_header,omitempty"`
50-
ExtraHeaderPrefix string `json:"extra_header_prefix"`
51-
}
52-
53-
type HttpHandler struct {
54-
index index.Index
55-
mapping []httpHandlerMapping
56-
defaultHandler http.Handler
57-
}
58-
59-
// httpHandlerMapping is used to route traffic to the correct backend server.
60-
// Higher weight means that the mapping is more specific and should be matched first.
61-
type httpHandlerMapping struct {
62-
weight int
63-
path string
64-
handler http.Handler
65-
}
66-
67-
func (h *HttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
68-
// mappings are used to route traffic to the correct backend server.
69-
// It should not have `/clusters` as prefix because that is handled by the
70-
// shardHandler or mounts. Logic is as follows:
71-
// 1. We detect URL for the request and find the correct handler. URL can be
72-
// shard based, virtual workspace or mount. First two are covered by r.URL,
73-
// where mounts are covered by annotation on the workspace with the mount path.
74-
// 2. If mountpoint is found, we rewrite the URL to resolve, else use one in
75-
// request to match with mappings.
76-
// 3. Iterate over mappings and find the one that matches the URL. If found,
77-
// use the handler for that mapping, else use default handler - kcp.
78-
// Mappings are done from most specific to least specific:
79-
// Example: /clusters/cluster1/ will be matched before /clusters/
80-
for _, m := range h.mapping {
81-
url, errorCode := h.resolveURL(r)
82-
if errorCode != 0 {
83-
http.Error(w, http.StatusText(errorCode), errorCode)
84-
return
85-
}
86-
if strings.HasPrefix(url, m.path) {
87-
m.handler.ServeHTTP(w, r)
88-
return
89-
}
90-
}
91-
92-
h.defaultHandler.ServeHTTP(w, r)
93-
}
94-
95-
func (h *HttpHandler) resolveURL(r *http.Request) (string, int) {
96-
// if we don't match any of the paths, use the default behavior - request
97-
var cs = strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 3)
98-
if len(cs) < 2 || cs[0] != "clusters" {
99-
return r.URL.Path, 0
100-
}
101-
102-
clusterPath := logicalcluster.NewPath(cs[1])
103-
if !clusterPath.IsValid() {
104-
return r.URL.Path, 0
105-
}
106-
107-
u, found, errCode := h.index.LookupURL(clusterPath)
108-
if errCode != 0 {
109-
return "", errCode
110-
}
111-
if found {
112-
u, err := url.Parse(u)
113-
if err == nil && u != nil {
114-
u.Path = strings.TrimSuffix(u.Path, "/")
115-
r.URL.Path = path.Join(u.Path, strings.Join(cs[2:], "/")) // override request prefix and keep kube api contextual suffix
116-
return u.Path, 0
117-
}
118-
}
119-
120-
return r.URL.Path, 0
121-
}
122-
12336
func NewHandler(ctx context.Context, o *proxyoptions.Options, index index.Index) (http.Handler, error) {
12437
mappingData, err := os.ReadFile(o.MappingFile)
12538
if err != nil {
12639
return nil, fmt.Errorf("failed to read mapping file %q: %w", o.MappingFile, err)
12740
}
12841

129-
var mapping []PathMapping
42+
var mapping []proxy.PathMapping
13043
if err = yaml.Unmarshal(mappingData, &mapping); err != nil {
13144
return nil, fmt.Errorf("failed to unmarshal mapping file %q: %w", o.MappingFile, err)
13245
}
13346

134-
handlers := HttpHandler{
135-
index: index,
136-
mapping: []httpHandlerMapping{
47+
handlers := proxy.HttpHandler{
48+
Index: index,
49+
Mappings: proxy.HttpHandlerMappings{
13750
{
138-
weight: 0,
139-
path: "/metrics",
140-
handler: legacyregistry.Handler(),
51+
Weight: 0,
52+
Path: "/metrics",
53+
Handler: legacyregistry.Handler(),
14154
},
14255
},
14356
}
@@ -185,31 +98,17 @@ func NewHandler(ctx context.Context, o *proxyoptions.Options, index index.Index)
18598

18699
logger.V(2).WithValues("path", m.Path).Info("adding handler")
187100
if m.Path == "/" {
188-
handlers.defaultHandler = handler
101+
handlers.DefaultHandler = handler
189102
} else {
190-
handlers.mapping = append(handlers.mapping, httpHandlerMapping{
191-
weight: len(m.Path),
192-
path: m.Path,
193-
handler: handler,
103+
handlers.Mappings = append(handlers.Mappings, proxy.HttpHandlerMapping{
104+
Weight: len(m.Path),
105+
Path: m.Path,
106+
Handler: handler,
194107
})
195108
}
196109
}
197110

198-
handlers.mapping = sortMappings(handlers.mapping)
111+
handlers.Mappings.Sort()
199112

200113
return &handlers, nil
201114
}
202-
203-
func sortMappings(mappings []httpHandlerMapping) []httpHandlerMapping {
204-
// sort mappings by weight
205-
// higher weight means that the mapping is more specific and should be matched first
206-
// Example: /clusters/cluster1/ will be matched before /clusters/
207-
for i := range mappings {
208-
for j := range mappings {
209-
if mappings[i].weight > mappings[j].weight {
210-
mappings[i], mappings[j] = mappings[j], mappings[i]
211-
}
212-
}
213-
}
214-
return mappings
215-
}

pkg/server/config.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,16 @@ func NewConfig(ctx context.Context, opts kcpserveroptions.CompletedOptions) (*Co
462462
apiHandler = mux
463463

464464
apiHandler = filters.WithAuditInit(apiHandler) // Must run before any audit annotation is made
465-
apiHandler = WithLocalProxy(apiHandler, opts.Extra.ShardName, opts.Extra.ShardBaseURL, c.KcpSharedInformerFactory.Tenancy().V1alpha1().Workspaces(), c.KcpSharedInformerFactory.Core().V1alpha1().LogicalClusters())
465+
apiHandler, err = WithLocalProxy(apiHandler,
466+
opts.Extra.ShardName,
467+
opts.Extra.ShardBaseURL,
468+
opts.Extra.AdditionalMappingsFile,
469+
c.KcpSharedInformerFactory.Tenancy().V1alpha1().Workspaces(),
470+
c.KcpSharedInformerFactory.Core().V1alpha1().LogicalClusters(),
471+
)
472+
if err != nil {
473+
panic(err) // shouldn't happen due to flag validation
474+
}
466475
apiHandler = WithInClusterServiceAccountRequestRewrite(apiHandler)
467476
apiHandler = kcpfilters.WithAcceptHeader(apiHandler)
468477
apiHandler = WithUserAgent(apiHandler)

0 commit comments

Comments
 (0)