diff --git a/backend/http/handler.go b/backend/http/handler.go index a1dbf192d..3ca2e8be1 100644 --- a/backend/http/handler.go +++ b/backend/http/handler.go @@ -18,6 +18,7 @@ package http import ( "bytes" + "context" "encoding/base64" "encoding/json" "errors" @@ -32,10 +33,12 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/securecookie" "golang.org/x/oauth2" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" componentbaseversion "k8s.io/component-base/version" "k8s.io/klog/v2" @@ -66,6 +69,7 @@ type handler struct { backendCallbackURL string providerPrettyName string testingAutoSelect string + schemaSource string cookieEncryptionKey []byte cookieSigningKey []byte @@ -78,6 +82,7 @@ func NewHandler( provider *OIDCServiceProvider, oidcAuthorizeURL, backendCallbackURL, providerPrettyName, testingAutoSelect string, cookieSigningKey, cookieEncryptionKey []byte, + schemaSource string, scope kubebindv1alpha2.InformerScope, mgr *kubernetes.Manager, ) (*handler, error) { @@ -87,6 +92,7 @@ func NewHandler( backendCallbackURL: backendCallbackURL, providerPrettyName: providerPrettyName, testingAutoSelect: testingAutoSelect, + schemaSource: schemaSource, scope: scope, client: http.DefaultClient, kubeManager: mgr, @@ -184,20 +190,24 @@ func generateCookieName(clusterID string) string { func (h *handler) handleAuthorize(w http.ResponseWriter, r *http.Request) { logger := getLogger(r) - cluster := mux.Vars(r)["cluster"] + providerCluster := mux.Vars(r)["cluster"] + + callbackPort := r.URL.Query().Get("p") scopes := []string{"openid", "profile", "email", "offline_access"} code := &AuthCode{ - RedirectURL: r.URL.Query().Get("u"), - SessionID: r.URL.Query().Get("s"), - ClusterID: cluster, + RedirectURL: r.URL.Query().Get("u"), + SessionID: r.URL.Query().Get("s"), + ClusterID: r.URL.Query().Get("c"), + ProviderClusterID: providerCluster, // used in multicluster-runtime providers } - if p := r.URL.Query().Get("p"); p != "" && code.RedirectURL == "" { - code.RedirectURL = fmt.Sprintf("http://localhost:%s/callback", p) + if callbackPort != "" && code.RedirectURL == "" { + code.RedirectURL = fmt.Sprintf("http://localhost:%s/callback", callbackPort) } - if code.RedirectURL == "" || code.SessionID == "" { + + if code.RedirectURL == "" || code.SessionID == "" || code.ClusterID == "" { logger.Error(errors.New("missing redirect url or session id or cluster id"), "failed to authorize") - http.Error(w, "missing redirect_url or session_id", http.StatusBadRequest) + http.Error(w, "missing redirect_url, session_id or cluster_id", http.StatusBadRequest) return } @@ -279,10 +289,10 @@ func (h *handler) handleCallback(w http.ResponseWriter, r *http.Request) { secure := false http.SetCookie(w, session.MakeCookie(r, cookieName, encoded, secure, 1*time.Hour)) - if authCode.ClusterID == "" { - http.Redirect(w, r, "/resources", http.StatusFound) + if authCode.ProviderClusterID == "" { + http.Redirect(w, r, "/resources?s="+cookieName, http.StatusFound) } else { - http.Redirect(w, r, "/clusters/"+authCode.ClusterID+"/resources", http.StatusFound) + http.Redirect(w, r, "/clusters/"+authCode.ProviderClusterID+"/resources?s="+cookieName, http.StatusFound) } } @@ -328,59 +338,91 @@ func createSessionState(authCode *AuthCode, token *oauth2.Token) (*session.State }, nil } +type UISchema struct { + Name string + Version string + Group string + Kind string + Scope string // "Namespaced" or "Cluster" + Resource string + + // SessionID + SessionID string +} + func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { logger := getLogger(r) prepareNoCache(w) - cluster := mux.Vars(r)["cluster"] - singleClusterScoped := cluster == "" + providerCluster := mux.Vars(r)["cluster"] + sessionID := r.URL.Query().Get("s") + singleClusterScoped := providerCluster == "" if h.testingAutoSelect != "" { parts := strings.SplitN(h.testingAutoSelect, ".", 2) if singleClusterScoped { http.Redirect(w, r, "/resources/"+parts[0]+"/"+parts[1], http.StatusFound) } else { - http.Redirect(w, r, "/clusters/"+cluster+"/resources/"+parts[0]+"/"+parts[1], http.StatusFound) + http.Redirect(w, r, "/clusters/"+providerCluster+"/resources/"+parts[0]+"/"+parts[1], http.StatusFound) } return } - labelSelector := labels.Set{ - resources.ExportedCRDsLabel: "true", - } - - crds, err := h.kubeManager.ListCustomResourceDefinitions(r.Context(), cluster, labelSelector.AsSelector()) + apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster) if err != nil { - logger.Error(err, "failed to list crds") + logger.Error(err, "failed to get dynamic resources") http.Error(w, "internal error", http.StatusInternalServerError) return } - sort.SliceStable(crds.Items, func(i, j int) bool { - return crds.Items[i].Name < crds.Items[j].Name - }) - rightScopedCRDs := []*apiextensionsv1.CustomResourceDefinition{} - for _, crd := range crds.Items { - if h.scope == kubebindv1alpha2.ClusterScope || crd.Spec.Scope == apiextensionsv1.NamespaceScoped { - rightScopedCRDs = append(rightScopedCRDs, &crd) + + var result []UISchema + for _, item := range apiResourceSchemas.Items { + scope := item.UnstructuredContent()["spec"].(map[string]interface{})["scope"] + if scope == nil { + scope = "-" } - } - apiResourceSchemas, err := h.kubeManager.ListAPIResourceSchemas(r.Context(), cluster) - if err != nil { - logger.Error(err, "failed to get api resource schemas") - http.Error(w, "internal error", http.StatusInternalServerError) - return + group := item.UnstructuredContent()["spec"].(map[string]interface{})["group"] + if group == nil { + group = "-" + } + resource := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["plural"] + if resource == nil { + resource = "-" + } + + kind := item.UnstructuredContent()["spec"].(map[string]interface{})["names"].(map[string]interface{})["kind"] + if kind == nil { + kind = "-" + } + + versions := item.UnstructuredContent()["spec"].(map[string]interface{})["versions"] + if versions == nil { + versions = []interface{}{""} + } + for _, v := range versions.([]interface{}) { + version := v.(map[string]interface{})["name"] + result = append(result, UISchema{ + Name: item.GetName(), + Kind: kind.(string), + Scope: scope.(string), + Version: version.(string), + Group: group.(string), + // Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it. + Resource: resource.(string), + SessionID: sessionID, + }) + } } + bs := bytes.Buffer{} if err := resourcesTemplate.Execute(&bs, struct { - Cluster string - CRDs []*apiextensionsv1.CustomResourceDefinition - APIResourceSchemas []kubebindv1alpha2.APIResourceSchema + Cluster string + Schemas []UISchema }{ - Cluster: cluster, - CRDs: rightScopedCRDs, - APIResourceSchemas: apiResourceSchemas.Items, + Cluster: providerCluster, + Schemas: result, }); err != nil { logger.Error(err, "failed to execute template") http.Error(w, "internal error", http.StatusInternalServerError) @@ -393,14 +435,16 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) { func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { logger := getLogger(r) + name := r.URL.Query().Get("name") group := r.URL.Query().Get("group") resource := r.URL.Query().Get("resource") - cluster := mux.Vars(r)["cluster"] + version := r.URL.Query().Get("version") + providerCluster := mux.Vars(r)["cluster"] prepareNoCache(w) - cookieName := generateCookieName(cluster) - ck, err := r.Cookie(cookieName) + cookieName := r.URL.Query().Get("s") + ck, err := r.Cookie(r.URL.Query().Get("s")) if err != nil { logger.Error(err, "failed to get session cookie") http.Error(w, "no session cookie found", http.StatusBadRequest) @@ -415,7 +459,38 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { return } - kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, cluster, resource, group) + // There is an intent to bind. We need to create APIResourceSchema if one does not exists. + { + apiResourceSchemas, err := h.getBackendDynamicResource(r.Context(), providerCluster) + if err != nil { + logger.Error(err, "failed to get dynamic resources") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + schema := &unstructured.Unstructured{} + for _, item := range apiResourceSchemas.Items { + if item.GetName() == name { + schema = &item + break + } + } + if schema == nil || schema.GetName() != name { + logger.Error(nil, "no APIResourceSchema found", "name", name, "group", group, "resource", resource, "version", version) + http.Error(w, fmt.Sprintf("no APIResourceSchema found for %s.%s.%s/%s", group, resource, version, name), http.StatusNotFound) + return + } + + // create apiResourceSchema if not exists + err = h.kubeManager.CreateAPIResourceSchema(r.Context(), providerCluster, name, schema) + if err != nil && !apierrors.IsAlreadyExists(err) { + logger.Error(err, "failed to create APIResourceSchema") + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + } + + kfg, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject+"#"+state.ClusterID, providerCluster) if err != nil { logger.Error(err, "failed to handle resources") http.Error(w, "internal error", http.StatusInternalServerError) @@ -435,7 +510,10 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) { }, Spec: kubebindv1alpha2.APIServiceExportRequestSpec{ Resources: []kubebindv1alpha2.APIServiceExportRequestResource{ - {GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource}}, + { + GroupResource: kubebindv1alpha2.GroupResource{Group: group, Resource: resource}, + Versions: []string{version}, + }, }, }, } @@ -495,3 +573,28 @@ func mustRead(f func(name string) ([]byte, error), name string) string { } return string(bs) } + +func (h *handler) getBackendDynamicResource(ctx context.Context, cluster string) (*unstructured.UnstructuredList, error) { + labelSelector := labels.Set{ + resources.ExportedCRDsLabel: "true", + } + + parts := strings.SplitN(h.schemaSource, ".", 3) + if len(parts) != 3 { // We check this in validation, but just in case. + return nil, fmt.Errorf("invalid schema source: %q", h.schemaSource) + } + + gvk := schema.GroupVersionKind{ + Kind: parts[0], + Version: parts[1], + Group: parts[2], + } + apiResourceSchemas, err := h.kubeManager.ListDynamicResources(ctx, cluster, gvk, labelSelector.AsSelector()) + if err != nil { + return nil, fmt.Errorf("failed to list crds: %w", err) + } + sort.SliceStable(apiResourceSchemas.Items, func(i, j int) bool { + return apiResourceSchemas.Items[i].GetName() < apiResourceSchemas.Items[j].GetName() + }) + return apiResourceSchemas, nil +} diff --git a/backend/http/oidc.go b/backend/http/oidc.go index 11cf4d179..30256b538 100644 --- a/backend/http/oidc.go +++ b/backend/http/oidc.go @@ -26,9 +26,10 @@ import ( // AuthCode is sent and received by to/from the OIDC provider. It's the state // we can use to map the OIDC provider's response to the request from the client. type AuthCode struct { - RedirectURL string `json:"redirectURL"` - SessionID string `json:"sid"` - ClusterID string `json:"cid"` + RedirectURL string `json:"redirectURL"` + SessionID string `json:"sid"` + ClusterID string `json:"cid"` + ProviderClusterID string `json:"pcid"` } type OIDCServiceProvider struct { diff --git a/backend/kubernetes/manager.go b/backend/kubernetes/manager.go index 57fa95731..ff7795301 100644 --- a/backend/kubernetes/manager.go +++ b/backend/kubernetes/manager.go @@ -19,11 +19,15 @@ package kubernetes import ( "context" "fmt" + "strings" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,6 +40,7 @@ import ( type Manager struct { namespacePrefix string providerPrettyName string + scope kubebindv1alpha2.InformerScope externalAddressGenerator kuberesources.ExternalAddreesGeneratorFunc externalCA []byte @@ -48,6 +53,7 @@ func NewKubernetesManager( ctx context.Context, namespacePrefix, providerPrettyName string, externalAddressGenerator kuberesources.ExternalAddreesGeneratorFunc, + scope kubebindv1alpha2.InformerScope, externalCA []byte, externalTLSServerName string, manager mcmanager.Manager, @@ -55,6 +61,7 @@ func NewKubernetesManager( m := &Manager{ namespacePrefix: namespacePrefix, providerPrettyName: providerPrettyName, + scope: scope, externalAddressGenerator: externalAddressGenerator, externalCA: externalCA, @@ -71,8 +78,8 @@ func NewKubernetesManager( return m, nil } -func (m *Manager) HandleResources(ctx context.Context, identity, cluster, resource, group string) ([]byte, error) { - logger := klog.FromContext(ctx).WithValues("identity", identity, "resource", resource, "group", group) +func (m *Manager) HandleResources(ctx context.Context, identity, cluster string) ([]byte, error) { + logger := klog.FromContext(ctx).WithValues("identity", identity) ctx = klog.NewContext(ctx, logger) cl, err := m.manager.GetCluster(ctx, cluster) @@ -173,3 +180,51 @@ func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster str return &crds, nil } + +func (m *Manager) ListDynamicResources(ctx context.Context, cluster string, gvk schema.GroupVersionKind, selector labels.Selector) (*unstructured.UnstructuredList, error) { + cl, err := m.manager.GetCluster(ctx, cluster) + if err != nil { + return nil, err + } + c := cl.GetClient() + + // Ensure we have the List kind + listGVK := gvk + if !strings.HasSuffix(listGVK.Kind, "List") { + listGVK.Kind += "List" + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(listGVK) + + listOpts := []client.ListOption{} + if selector != nil { + listOpts = append(listOpts, client.MatchingLabelsSelector{Selector: selector}) + } + + if err := c.List(ctx, list, listOpts...); err != nil { + return nil, err + } + + return list, nil +} + +func (m *Manager) CreateAPIResourceSchema(ctx context.Context, cluster string, name string, u *unstructured.Unstructured) error { + cl, err := m.manager.GetCluster(ctx, cluster) + if err != nil { + return err + } + c := cl.GetClient() + + apiResourceSchema := &kubebindv1alpha2.APIResourceSchema{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), apiResourceSchema) + if err != nil { + return err + } + + apiResourceSchema.ResourceVersion = "" + apiResourceSchema.Name = name + apiResourceSchema.Spec.InformerScope = m.scope + + return c.Create(ctx, apiResourceSchema) +} diff --git a/backend/options/options.go b/backend/options/options.go index 5512c1678..65d0ba651 100644 --- a/backend/options/options.go +++ b/backend/options/options.go @@ -53,6 +53,11 @@ type ExtraOptions struct { ExternalCAFile string ExternalCA []byte TLSExternalServerName string + // Defines the source of the schema for the bind screen. + // Options are: + // CustomResourceDefinition.v1.apiextensions.k8s.io + // APIResourceSchema.v1alpha2.kube-bind.io + SchemaSource string TestingAutoSelect string TestingSkipNameValidation bool @@ -89,6 +94,7 @@ func NewOptions() *Options { ConsumerScope: string(kubebindv1alpha2.NamespacedScope), ClusterScopedIsolation: string(kubebindv1alpha2.IsolationPrefixed), ServerURL: "", + SchemaSource: CustomResourceDefinitionSource.String(), }, } } @@ -99,6 +105,23 @@ var providerAliases = map[string]string{ "": "kubernetes", } +type SchemaSource string + +func (s SchemaSource) String() string { + return string(s) +} + +var ( + APIResourceSchemaSource = SchemaSource("APIResourceSchema.v1alpha2.kube-bind.io") + CustomResourceDefinitionSource = SchemaSource("CustomResourceDefinition.v1.apiextensions.k8s.io") +) + +var schemaSourceAliases = map[string]string{ + CustomResourceDefinitionSource.String(): CustomResourceDefinitionSource.String(), + "apiresourceschema": APIResourceSchemaSource.String(), + "customresourcedefinition": CustomResourceDefinitionSource.String(), +} + func (options *Options) AddFlags(fs *pflag.FlagSet) { logsv1.AddFlags(options.Logs, fs) options.OIDC.AddFlags(fs) @@ -118,6 +141,16 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fmt.Sprintf("The multicluster runtime provider. Possible values are: %v", sets.List(sets.Set[string](sets.StringKeySet(providerAliases)))), ) + values := make([]string, 0, len(schemaSourceAliases)) + for _, v := range schemaSourceAliases { + values = append(values, v) + } + + fs.StringVar(&options.SchemaSource, "schema-source", options.SchemaSource, + fmt.Sprintf("Defines the source of the schema in Kind.Version.Group format for the bind screen. Defaults to CustomResourceDefinition.v1.apiextensions.k8s.io. Possible values are: %v", + values), + ) + fs.StringVar(&options.ServerURL, "server-url", options.ServerURL, "The URL of the backend server. If not specified, it will be derived from the kubeconfig or service account's hosts.") fs.StringVar(&options.TestingAutoSelect, "testing-auto-select", options.TestingAutoSelect, ". that is automatically selected on th bind screen for testing") @@ -206,5 +239,15 @@ func (options *CompletedOptions) Validate() error { } options.Provider = provider + schemaSource := schemaSourceAliases[options.SchemaSource] + if schemaSource == "" { + return fmt.Errorf("unknown schema source %q, must be one of %v", options.SchemaSource, sets.List(sets.Set[string](sets.StringKeySet(schemaSourceAliases)))) + } + options.SchemaSource = schemaSource + parts := strings.SplitN(options.SchemaSource, ".", 3) + if len(parts) != 3 { // We check this in validation, but just in case. + return fmt.Errorf("invalid schema source: %q", options.SchemaSource) + } + return nil } diff --git a/backend/server.go b/backend/server.go index 4f1d06dc5..d16eb4a32 100644 --- a/backend/server.go +++ b/backend/server.go @@ -32,17 +32,17 @@ import ( "github.com/kube-bind/kube-bind/backend/controllers/serviceexport" "github.com/kube-bind/kube-bind/backend/controllers/serviceexportrequest" "github.com/kube-bind/kube-bind/backend/controllers/servicenamespace" - examplehttp "github.com/kube-bind/kube-bind/backend/http" - examplekube "github.com/kube-bind/kube-bind/backend/kubernetes" + http "github.com/kube-bind/kube-bind/backend/http" + kube "github.com/kube-bind/kube-bind/backend/kubernetes" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" ) type Server struct { Config *Config - OIDC *examplehttp.OIDCServiceProvider - Kubernetes *examplekube.Manager - WebServer *examplehttp.Server + OIDC *http.OIDCServiceProvider + Kubernetes *kube.Manager + WebServer *http.Server Controllers } @@ -60,7 +60,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { } var err error - s.WebServer, err = examplehttp.NewServer(c.Options.Serve) + s.WebServer, err = http.NewServer(c.Options.Serve) if err != nil { return nil, fmt.Errorf("error setting up HTTP Server: %w", err) } @@ -70,7 +70,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { if callback == "" { callback = fmt.Sprintf("http://%s/callback", s.WebServer.Addr().String()) } - s.OIDC, err = examplehttp.NewOIDCServiceProvider( + s.OIDC, err = http.NewOIDCServiceProvider( c.Options.OIDC.IssuerClientID, c.Options.OIDC.IssuerClientSecret, callback, @@ -79,11 +79,12 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { if err != nil { return nil, fmt.Errorf("error setting up OIDC: %w", err) } - s.Kubernetes, err = examplekube.NewKubernetesManager( + s.Kubernetes, err = kube.NewKubernetesManager( ctx, c.Options.NamespacePrefix, c.Options.PrettyName, c.ExternalAddressGenerator, + kubebindv1alpha2.InformerScope(c.Options.ExtraOptions.ConsumerScope), c.Options.ExternalCA, c.Options.TLSExternalServerName, s.Config.Manager, @@ -106,7 +107,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { } } - handler, err := examplehttp.NewHandler( + handler, err := http.NewHandler( s.OIDC, c.Options.OIDC.AuthorizeURL, callback, @@ -114,6 +115,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { c.Options.TestingAutoSelect, signingKey, encryptionKey, + c.Options.SchemaSource, kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), s.Kubernetes, ) @@ -126,7 +128,6 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { SkipNameValidation: ptr.To(c.Options.TestingSkipNameValidation), } - // construct controllers s.ClusterBinding, err = clusterbinding.NewClusterBindingReconciler( ctx, s.Config.Manager, diff --git a/backend/template/resources.gohtml b/backend/template/resources.gohtml index 5197f4c07..4a3b87109 100644 --- a/backend/template/resources.gohtml +++ b/backend/template/resources.gohtml @@ -11,36 +11,20 @@ Resources - CRD -

CRD

+

Exports

- {{range .CRDs}} + {{range .Schemas}}
-

{{.Spec.Names.Singular}}

+

{{.Name}}

    -
  • Group: {{.Spec.Group}}
  • -
  • Scope: {{.Spec.Scope}}
  • +
  • Scope: {{.Scope}}
- Bind + Bind
{{end}}
-

APIResourceSchema

-
- {{range .APIResourceSchemas}} -
-

{{.Spec.Names.Singular}}

-
    -
  • Group: {{.Spec.Group}}
  • -
  • Scope: {{.Spec.Scope}}
  • -
-
- Bind -
-
- {{end}} -
diff --git a/contrib/example-backend/session/session.go b/contrib/example-backend/session/session.go deleted file mode 100644 index 387f73d93..000000000 --- a/contrib/example-backend/session/session.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2022 The Kube Bind Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package session - -import ( - "github.com/vmihailenco/msgpack/v4" -) - -// State is the data stored on the clientside as a browser cookie. -// To be independent of the OIDC token sizes, it only stores the absolute minimum -// information required for this particular backend implementation. Especially -// when lots of groups and claims are involved, the tokens can grow to a size -// larger than allowed by browsers for cookies, and even compression would only -// be a small adhesive strip, not a solution. -type State struct { - Token TokenInfo `msgpack:"tok,omitempty"` - SessionID string `msgpack:"sid,omitempty"` - ClusterID string `msgpack:"cl,omitempty"` - RedirectURL string `msgpack:"red,omitempty"` -} - -type TokenInfo struct { - Subject string `msgpack:"sub,omitempty"` - Issuer string `msgpack:"iss,omitempty"` -} - -func (s *State) Encode() ([]byte, error) { - return msgpack.Marshal(s) -} diff --git a/deploy/examples/crd-foo.yaml b/deploy/examples/crd-foo.yaml new file mode 100644 index 000000000..32dbcb06e --- /dev/null +++ b/deploy/examples/crd-foo.yaml @@ -0,0 +1,47 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: foos.bar.io + labels: + kube-bind.io/exported: "true" +spec: + group: bar.io + versions: + - name: v1alpha1 + served: true + storage: true + schema: + # schema used for validation + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + deploymentName: + type: string + replicas: + type: integer + minimum: 1 + maximum: 10 + status: + type: object + properties: + availableReplicas: + type: integer + phase: + type: string + enum: + - Pending + - Running + - Succeeded + - Failed + - Unknown + # subresources for the custom resource + subresources: + # enables the status subresource + status: {} + names: + kind: Foo + plural: foos + scope: Cluster diff --git a/deploy/examples/foo.yaml b/deploy/examples/foo.yaml new file mode 100644 index 000000000..916270e30 --- /dev/null +++ b/deploy/examples/foo.yaml @@ -0,0 +1,7 @@ +apiVersion: bar.io/v1alpha1 +kind: Foo +metadata: + name: test +spec: + deploymentName: test-foo + replicas: 2 \ No newline at end of file diff --git a/deploy/examples/mangodb.yaml b/deploy/examples/mangodb.yaml index e29cebcfb..4c6b8d334 100644 --- a/deploy/examples/mangodb.yaml +++ b/deploy/examples/mangodb.yaml @@ -1,12 +1,9 @@ apiVersion: mangodb.com/v1alpha1 kind: MangoDB - metadata: name: my-first-mangodb-instance namespace: default - spec: tier: Dedicated - status: phase: Pending \ No newline at end of file diff --git a/docs/content/setup/quickstart.md b/docs/content/setup/quickstart.md index 1a7256948..a55b4c418 100644 --- a/docs/content/setup/quickstart.md +++ b/docs/content/setup/quickstart.md @@ -60,6 +60,7 @@ kubectl ws create provider --enter ```shell kubectl apply -f deploy/crd kubectl apply -f deploy/examples/crd-mangodb.yaml +kubectl apply -f deploy/examples/crd-foo.yaml ``` * start the backend binary with the right flags: @@ -73,7 +74,8 @@ bin/backend \ --pretty-name="BigCorp.com" \ --namespace-prefix="kube-bind-" \ --cookie-signing-key=bGMHz7SR9XcI9JdDB68VmjQErrjbrAR9JdVqjAOKHzE= \ - --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= + --cookie-encryption-key=wadqi4u+w0bqnSrVFtM38Pz2ykYVIeeadhzT34XlC1Y= \ + --consumer-scope=cluster ``` where `ZXhhbXBsZS1hcHAtc2VjcmV0` matches the value of the dex config file. @@ -91,17 +93,25 @@ The `--cookie-encryption-key` option is optional and supports byte lengths of 16 Now create consumer cluster: ```shell -$ export KUBECONFIG=.kcp/admin.kubeconfig -$ kubectl ws create consumer --enter +export KUBECONFIG=.kcp/admin.kubeconfig +kubectl ws create consumer --enter ``` Now create the APIServiceExportRequest: ```shell -$ ./bin/kubectl-bind http://127.0.0.1:8080/exports --dry-run -o yaml > apiserviceexport.yaml +./bin/kubectl-bind http://127.0.0.1:8080/exports --dry-run -o yaml > apiserviceexport.yaml # This will wait for konnector to be ready. Once this gets running - start the konnector bellow # IMPORTANT: Check namespace to be used! -$ ./bin/kubectl-bind apiservice --remote-kubeconfig .kcp/provider.kubeconfig -f apiserviceexport.yaml --skip-konnector --remote-namespace -# run konnector -$ go run ./cmd/konnector/ --lease-namespace default +./bin/kubectl-bind apiservice --remote-kubeconfig .kcp/provider.kubeconfig -f apiserviceexport.yaml --skip-konnector --remote-namespace +# run konnector in different terminal +export KUBECONFIG=.kcp/admin.kubeconfig +go run ./cmd/konnector/ --lease-namespace default ``` + + +Create mangoDB object: + +```bash +kubectl create -f deploy/examples/mangodb.yaml +``` \ No newline at end of file diff --git a/kcp/README.md b/kcp/README.md index ae4fe179e..2f100f725 100644 --- a/kcp/README.md +++ b/kcp/README.md @@ -26,8 +26,8 @@ It will do the following: 1. Start kcp 2. Bootstrap kcp: ```bash -cp .kcp/admin.kubeconfig .kcp/provider.kubeconfig -export KUBECONFIG=.kcp/provider.kubeconfig +cp .kcp/admin.kubeconfig .kcp/backend.kubeconfig +export KUBECONFIG=.kcp/backend.kubeconfig ./bin/kcp-init --kcp-kubeconfig $KUBECONFIG ``` 3. Run the backend: @@ -48,14 +48,14 @@ bin/backend \ ``` -4. Copy the kubeconfig to the consumer: +4. Copy the kubeconfig to the provider: ```bash -cp .kcp/admin.kubeconfig .kcp/sub-provider.kubeconfig -export KUBECONFIG=.kcp/sub-provider.kubeconfig +cp .kcp/admin.kubeconfig .kcp/provider.kubeconfig +export KUBECONFIG=.kcp/provider.kubeconfig k ws use :root ``` -5. Run `kubectl ws create sub-provider --enter` +5. Run `kubectl ws create provider --enter` 6. Bind the APIExport to the workspace ```bash kubectl kcp bind apiexport root:kube-bind:kube-bind.io --accept-permission-claim clusterrolebindings.rbac.authorization.k8s.io \ @@ -72,7 +72,7 @@ kubectl kcp bind apiexport root:kube-bind:kube-bind.io --accept-permission-claim 7. Create CRD: ```bash -kubectl apply -f deploy/examples/crd-mongodb.yaml +kubectl apply -f deploy/examples/crd-mangodb.yaml ``` 8. Get LogicalCluster: @@ -94,9 +94,10 @@ kubectl ws create consumer --enter 10. Bind the thing: ```bash -./bin/kubectl-bind http://127.0.0.1:8080/clusters/2xh2v3gzjhn4tmve/exports --dry-run -o yaml > apiserviceexport.yaml +./bin/kubectl-bind http://127.0.0.1:8080/clusters/2vgrh380y0cq38du/exports --dry-run -o yaml > apiserviceexport.yaml -# we need dedicated secret for that +# Extract secret for binding process. Note that secret name is not the same as output from command above. Check secret +# name by running `kubectl get secret -n kube-bind` kubectl get secret kubeconfig-wvvsb -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote.kubeconfig ./bin/kubectl-bind apiservice --remote-kubeconfig remote.kubeconfig -f apiserviceexport.yaml --skip-konnector --remote-namespace kube-bind-m5zx4 @@ -104,6 +105,31 @@ kubectl get secret kubeconfig-wvvsb -n kube-bind -o jsonpath='{.data.kubeconfig} export KUBECONFIG=.kcp/consumer.kubeconfig go run ./cmd/konnector/ --lease-namespace default + +11. (Optional) Add second consumer to test + +```bash +cp .kcp/admin.kubeconfig .kcp/consumer2.kubeconfig +export KUBECONFIG=.kcp/consumer2.kubeconfig +kubectl ws use :root +kubectl ws create consumer2 --enter + +./bin/kubectl-bind http://127.0.0.1:8080/clusters/2vgrh380y0cq38du/exports --dry-run -o yaml > apiserviceexport2.yaml +kubectl get secret kubeconfig-wvvsb -n kube-bind -o jsonpath='{.data.kubeconfig}' | base64 -d > remote2.kubeconfig + +./bin/kubectl-bind apiservice --remote-kubeconfig remote2.kubeconfig -f apiserviceexport.yaml --skip-konnector --remote-namespace kube-bind-m5zx4 + + +export KUBECONFIG=.kcp/consumer2.kubeconfig +go run ./cmd/konnector/ --lease-namespace default --server-address :8091 +``` + +Create objects: +``` +kubectl create -f deploy/examples/mangodb.yaml +``` + + ## Debug ```bash diff --git a/pkg/konnector/config.go b/pkg/konnector/config.go index 380cb2a8f..a7513cc3d 100644 --- a/pkg/konnector/config.go +++ b/pkg/konnector/config.go @@ -40,10 +40,14 @@ type Config struct { KubeInformers kubeinformers.SharedInformerFactory BindInformers bindinformers.SharedInformerFactory ApiextensionsInformers apiextensionsinformers.SharedInformerFactory + + ServerAddr string } func NewConfig(options *options.CompletedOptions) (*Config, error) { - config := &Config{} + config := &Config{ + ServerAddr: options.ServerAddr, + } // create clients rules := clientcmd.NewDefaultClientConfigLoadingRules() diff --git a/pkg/konnector/options/options.go b/pkg/konnector/options/options.go index 9821b794e..825f12ce9 100644 --- a/pkg/konnector/options/options.go +++ b/pkg/konnector/options/options.go @@ -38,6 +38,8 @@ type ExtraOptions struct { LeaseLockName string LeaseLockNamespace string LeaseLockIdentity string + + ServerAddr string } type completedOptions struct { @@ -78,6 +80,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&options.KubeConfigPath, "kubeconfig", options.KubeConfigPath, "Kubeconfig file for the local cluster.") fs.StringVar(&options.LeaseLockName, "lease-name", options.LeaseLockName, "Name of lease lock") fs.StringVar(&options.LeaseLockNamespace, "lease-namespace", options.LeaseLockNamespace, "Name of lease lock namespace") + fs.StringVar(&options.ServerAddr, "server-address", options.ServerAddr, "Address for server") } func (options *Options) Complete() (*CompletedOptions, error) { diff --git a/pkg/konnector/server.go b/pkg/konnector/server.go index 532d6e80a..aa55e9b78 100644 --- a/pkg/konnector/server.go +++ b/pkg/konnector/server.go @@ -24,7 +24,7 @@ import ( "k8s.io/klog/v2" "github.com/kube-bind/kube-bind/deploy/crd" - healthz "github.com/kube-bind/kube-bind/pkg/konnector/healthz" + server "github.com/kube-bind/kube-bind/pkg/konnector/server" kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2" ) @@ -32,7 +32,7 @@ type Server struct { Config *Config Controller *Controller - webServer *healthz.Server + webServer *server.Server } func NewServer(config *Config) (*Server, error) { @@ -53,10 +53,9 @@ func NewServer(config *Config) (*Server, error) { Controller: k, } - s.webServer, err = healthz.NewServer() - if err != nil { - return nil, fmt.Errorf("error setting up HTTP Server: %w", err) - } + s.webServer = server.NewServer(server.Config{ + ServerAddr: config.ServerAddr, + }) return s, nil } @@ -65,7 +64,7 @@ func (s *Server) StartHealthCheck(ctx context.Context) { s.webServer.Start(ctx) } -func (s *Server) AddCheck(check healthz.HealthChecker) { +func (s *Server) AddCheck(check server.HealthChecker) { s.webServer.Checker.AddCheck(check) } diff --git a/pkg/konnector/healthz/checker.go b/pkg/konnector/server/checker.go similarity index 98% rename from pkg/konnector/healthz/checker.go rename to pkg/konnector/server/checker.go index cf7101a43..ea343e97f 100644 --- a/pkg/konnector/healthz/checker.go +++ b/pkg/konnector/server/checker.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package healthz +package server import ( "encoding/json" diff --git a/pkg/konnector/healthz/server.go b/pkg/konnector/server/server.go similarity index 76% rename from pkg/konnector/healthz/server.go rename to pkg/konnector/server/server.go index c8cec44d8..55e5bf622 100644 --- a/pkg/konnector/healthz/server.go +++ b/pkg/konnector/server/server.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package healthz +package server import ( "context" @@ -31,15 +31,27 @@ const listenAddr = ":8090" type Server struct { Router *mux.Router Checker checker + + ServerAddr string +} + +type Config struct { + ServerAddr string } -func NewServer() (*Server, error) { +func NewServer(cfg Config) *Server { server := &Server{ - Router: mux.NewRouter(), - Checker: checker{}, + Router: mux.NewRouter(), + Checker: checker{}, + ServerAddr: cfg.ServerAddr, + } + server.Router.Handle("/healthz", server.Checker) + + if server.ServerAddr == "" { + server.ServerAddr = listenAddr } - return server, nil + return server } func (s *Server) Start(ctx context.Context) { @@ -49,8 +61,7 @@ func (s *Server) Start(ctx context.Context) { ReadHeaderTimeout: 1 * time.Minute, } go func() { - //nolint:gosec - listener, err := net.Listen("tcp", listenAddr) + listener, err := net.Listen("tcp", s.ServerAddr) if err != nil { panic(err) } diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index ec21b3185..3ee266312 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -76,6 +76,7 @@ func testHappyCase(t *testing.T, resourceScope apiextensionsv1.ResourceScope, in if resourceScope == apiextensionsv1.ClusterScoped { serviceGVR = schema.GroupVersionResource{Group: "bar.io", Version: "v1alpha1", Resource: "foos"} } + consumerClient := framework.DynamicClient(t, consumerConfig).Resource(serviceGVR) providerClient := framework.DynamicClient(t, providerConfig).Resource(serviceGVR) diff --git a/test/e2e/framework/backend.go b/test/e2e/framework/backend.go index 3c1549ba9..0903c65ac 100644 --- a/test/e2e/framework/backend.go +++ b/test/e2e/framework/backend.go @@ -72,24 +72,26 @@ func StartBackendWithoutDefaultArgs(t *testing.T, clientConfig *rest.Config, arg require.NoError(t, err) fs := pflag.NewFlagSet("example-backend", pflag.ContinueOnError) - options := options.NewOptions() - options.AddFlags(fs) + opts := options.NewOptions() + opts.AddFlags(fs) err = fs.Parse(args) require.NoError(t, err) // use a random port via an explicit listener. Then add a kube-bind- client to dex // with the callback URL set to the listener's address. - options.Serve.Listener, err = net.Listen("tcp", "localhost:0") + opts.Serve.Listener, err = net.Listen("tcp", "localhost:0") require.NoError(t, err) - addr := options.Serve.Listener.Addr() + addr := opts.Serve.Listener.Addr() _, port, err := net.SplitHostPort(addr.String()) require.NoError(t, err) - options.OIDC.IssuerClientID = "kube-bind-" + port + + opts.OIDC.IssuerClientID = "kube-bind-" + port createDexClient(t, addr) - options.ExtraOptions.TestingSkipNameValidation = true + opts.ExtraOptions.TestingSkipNameValidation = true + opts.ExtraOptions.SchemaSource = options.CustomResourceDefinitionSource.String() - completed, err := options.Complete() + completed, err := opts.Complete() require.NoError(t, err) config, err := backend.NewConfig(completed)