Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ type APIServiceExportRequestReconciler struct {
manager mcmanager.Manager
opts controller.TypedOptions[mcreconcile.Request]

informerScope kubebindv1alpha2.InformerScope
clusterScopedIsolation kubebindv1alpha2.Isolation
Comment thread
mjudeikis marked this conversation as resolved.
reconciler reconciler
informerScope kubebindv1alpha2.InformerScope
isolation kubebindv1alpha2.Isolation
reconciler reconciler
}

// NewAPIServiceExportRequestReconciler returns a new APIServiceExportRequestReconciler to reconcile APIServiceExportRequests.
Expand All @@ -75,14 +75,14 @@ func NewAPIServiceExportRequestReconciler(
}

r := &APIServiceExportRequestReconciler{
manager: mgr,
opts: opts,
informerScope: scope,
clusterScopedIsolation: isolation,
manager: mgr,
opts: opts,
informerScope: scope,
isolation: isolation,
reconciler: reconciler{
informerScope: scope,
clusterScopedIsolation: isolation,
schemaSource: schemaSource,
informerScope: scope,
isolation: isolation,
schemaSource: schemaSource,
getBoundSchema: func(ctx context.Context, cl client.Client, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) {
var schema kubebindv1alpha2.BoundSchema
key := types.NamespacedName{Namespace: namespace, Name: name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ import (
)

type reconciler struct {
informerScope kubebindv1alpha2.InformerScope
clusterScopedIsolation kubebindv1alpha2.Isolation
schemaSource string
informerScope kubebindv1alpha2.InformerScope
isolation kubebindv1alpha2.Isolation
schemaSource string

getBoundSchema func(ctx context.Context, cl client.Client, namespace, name string) (*kubebindv1alpha2.BoundSchema, error)
createBoundSchema func(ctx context.Context, cl client.Client, schema *kubebindv1alpha2.BoundSchema) error
Expand Down Expand Up @@ -134,7 +134,7 @@ func (r *reconciler) getExportedSchemas(ctx context.Context, cl client.Client) (
return boundSchemas, nil
}

func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, cache cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error {
func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, _ cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error {
exportedSchemas, err := r.getExportedSchemas(ctx, cl)
if err != nil {
return err
Expand Down Expand Up @@ -167,7 +167,7 @@ func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, c
// we need to rewrite the BoundSchema's scope accordingly. For all
// other isolation strategies, as well as for namespaced schemas,
// no changes are necessary.
if boundSchema.Spec.Scope == apiextensionsv1.NamespaceScoped && r.clusterScopedIsolation == kubebindv1alpha2.IsolationNamespaced {
if boundSchema.Spec.Scope == apiextensionsv1.NamespaceScoped && r.isolation == kubebindv1alpha2.IsolationNamespaced {
boundSchema.Spec.Scope = apiextensionsv1.ClusterScoped
}

Expand All @@ -185,7 +185,6 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
logger := klog.FromContext(ctx)

var schemas []*kubebindv1alpha2.BoundSchema
var scope apiextensionsv1.ResourceScope
if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhasePending {
for _, res := range req.Spec.Resources {
name := res.ResourceGroupName()
Expand All @@ -207,7 +206,6 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache

// Collect all schemas for hashing.
// TODO(mjudeikis) Scope is same for all crds so we keep stamping it over. We might want to change this
scope = boundSchema.Spec.Scope
schemas = append(schemas, boundSchema)
}

Expand Down Expand Up @@ -236,11 +234,9 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache
},
Spec: kubebindv1alpha2.APIServiceExportSpec{
InformerScope: r.informerScope,
Isolation: r.isolation,
},
}
if scope == apiextensionsv1.ClusterScoped {
export.Spec.ClusterScopedIsolation = r.clusterScopedIsolation
}

for _, res := range req.Spec.Resources {
export.Spec.Resources = append(export.Spec.Resources, kubebindv1alpha2.APIServiceExportResource{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ type APIServiceNamespaceReconciler struct {
manager mcmanager.Manager
opts controller.TypedOptions[mcreconcile.Request]

informerScope kubebindv1alpha2.InformerScope
clusterScopedIsolation kubebindv1alpha2.Isolation
reconciler reconciler
informerScope kubebindv1alpha2.InformerScope
isolation kubebindv1alpha2.Isolation
reconciler reconciler
}

// NewAPIServiceNamespaceReconciler returns a new APIServiceNamespaceReconciler to reconcile APIServiceNamespaces.
Expand All @@ -70,12 +70,13 @@ func NewAPIServiceNamespaceReconciler(
}

r := &APIServiceNamespaceReconciler{
manager: mgr,
opts: opts,
informerScope: scope,
clusterScopedIsolation: isolation,
manager: mgr,
opts: opts,
informerScope: scope,
isolation: isolation,
reconciler: reconciler{
scope: scope,
scope: scope,
isolation: isolation,

getNamespace: func(ctx context.Context, cache cache.Cache, name string) (*corev1.Namespace, error) {
var ns corev1.Namespace
Expand Down
18 changes: 14 additions & 4 deletions backend/controllers/servicenamespace/servicenamespace_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import (
)

type reconciler struct {
scope kubebindv1alpha2.InformerScope
scope kubebindv1alpha2.InformerScope
isolation kubebindv1alpha2.Isolation

getNamespace func(ctx context.Context, cache cache.Cache, name string) (*corev1.Namespace, error)
createNamespace func(ctx context.Context, client client.Client, ns *corev1.Namespace) error
Expand All @@ -52,11 +53,20 @@ type reconciler struct {

func (c *reconciler) reconcile(ctx context.Context, client client.Client, cache cache.Cache, sns *kubebindv1alpha2.APIServiceNamespace) error {
var ns *corev1.Namespace
nsName := sns.Namespace + "-" + sns.Name
if sns.Status.Namespace != "" {
var nsName string
switch {
case sns.Status.Namespace != "":
// use existing namespace from status
nsName = sns.Status.Namespace
ns, _ = c.getNamespace(ctx, cache, nsName) // golint:errcheck
case c.isolation == kubebindv1alpha2.IsolationNone:
nsName = sns.Name
case c.isolation == kubebindv1alpha2.IsolationNamespaced || c.isolation == kubebindv1alpha2.IsolationPrefixed:
nsName = sns.Namespace + "-" + sns.Name
default:
return fmt.Errorf("unknown isolation strategy: %s", c.isolation)
}
ns, _ = c.getNamespace(ctx, cache, nsName)

if ns == nil {
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Expand Down
2 changes: 2 additions & 0 deletions backend/oidc/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func TestLoadTLSConfig_Success(t *testing.T) {
// Verify the TLS config was created
if config == nil {
t.Fatal("Expected non-nil TLS config")
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fatal() already ends the test early, no need for a return.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but linter does not like it :) something about return early to avoid NPE :D

}

if config.RootCAs == nil {
Expand Down Expand Up @@ -194,6 +195,7 @@ func TestLoadTLSConfig_MultipleCerts(t *testing.T) {
// Verify the TLS config was created
if config == nil {
t.Fatal("Expected non-nil TLS config")
return
}

if config.RootCAs == nil {
Expand Down
47 changes: 26 additions & 21 deletions backend/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ type ExtraOptions struct {

Provider string

NamespacePrefix string
PrettyName string
ConsumerScope string
ClusterScopedIsolation string
ExternalAddress string
ExternalCAFile string
ExternalCA []byte
TLSExternalServerName string
NamespacePrefix string
PrettyName string
ConsumerScope string
Isolation string
ExternalAddress string
ExternalCAFile string
ExternalCA []byte
TLSExternalServerName string
// Defines the source of the schema for the bind screen.
// Options are:
// CustomResourceDefinition.v1.apiextensions.k8s.io
Expand Down Expand Up @@ -101,14 +101,14 @@ func NewOptions() *Options {
ProviderKcp: providerkcp.NewOptions(),

ExtraOptions: ExtraOptions{
Provider: "kubernetes",
NamespacePrefix: "cluster-",
PrettyName: "Backend",
ConsumerScope: string(kubebindv1alpha2.NamespacedScope),
ClusterScopedIsolation: string(kubebindv1alpha2.IsolationPrefixed),
SchemaSource: CustomResourceDefinitionSource.String(),
Frontend: "embedded", // Not used, but indicates to use embedded frontend using SPA.
TokenExpiry: 1 * time.Hour,
Provider: "kubernetes",
NamespacePrefix: "cluster-",
PrettyName: "Backend",
ConsumerScope: string(kubebindv1alpha2.NamespacedScope),
Isolation: string(kubebindv1alpha2.IsolationPrefixed),
SchemaSource: CustomResourceDefinitionSource.String(),
Frontend: "embedded", // Not used, but indicates to use embedded frontend using SPA.
TokenExpiry: 1 * time.Hour,
},
}
return opts
Expand Down Expand Up @@ -151,7 +151,10 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&options.NamespacePrefix, "namespace-prefix", options.NamespacePrefix, "The prefix to use for cluster namespaces")
fs.StringVar(&options.PrettyName, "pretty-name", options.PrettyName, "Pretty name for the backend")
fs.StringVar(&options.ConsumerScope, "consumer-scope", options.ConsumerScope, "How consumers access the service provider cluster. In Kubernetes, \"namespaced\" allows namespace isolation. In kcp, \"cluster\" allows workspace isolation, and with that allows cluster-scoped resources to bind and it is generally more performant.")
fs.StringVar(&options.ClusterScopedIsolation, "cluster-scoped-isolation", options.ClusterScopedIsolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.")
// TODO(mjudeikis): remove deprecated flag in future release
fs.StringVar(&options.Isolation, "cluster-scoped-isolation", options.Isolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.")
_ = fs.MarkDeprecated("cluster-scoped-isolation", "use --isolation instead")
fs.StringVar(&options.Isolation, "isolation", options.Isolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.")
fs.StringVar(&options.ExternalAddress, "external-address", options.ExternalAddress, "The external address for the service provider cluster, including https:// and port. If not specified, service account's hosts are used.")
fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.")
fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.")
Expand Down Expand Up @@ -200,13 +203,15 @@ func (options *Options) Complete() (*CompletedOptions, error) {
if strings.ToLower(options.ConsumerScope) == "cluster" {
options.ConsumerScope = string(kubebindv1alpha2.ClusterScope)
}
switch strings.ToLower(options.ClusterScopedIsolation) {
switch strings.ToLower(options.Isolation) {
case "prefixed":
options.ClusterScopedIsolation = string(kubebindv1alpha2.IsolationPrefixed)
options.Isolation = string(kubebindv1alpha2.IsolationPrefixed)
case "namespaced":
options.ClusterScopedIsolation = string(kubebindv1alpha2.IsolationNamespaced)
options.Isolation = string(kubebindv1alpha2.IsolationNamespaced)
case "none":
options.ClusterScopedIsolation = string(kubebindv1alpha2.IsolationNone)
options.Isolation = string(kubebindv1alpha2.IsolationNone)
default:
options.Isolation = string(kubebindv1alpha2.IsolationNone)
}

if options.ExternalCAFile != "" && options.ExternalCA != nil {
Expand Down
4 changes: 2 additions & 2 deletions backend/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
s.Config.Manager,
opts,
kubebindv1alpha2.InformerScope(c.Options.ConsumerScope),
kubebindv1alpha2.Isolation(c.Options.ClusterScopedIsolation),
kubebindv1alpha2.Isolation(c.Options.Isolation),
)
if err != nil {
return nil, fmt.Errorf("error setting up APIServiceNamespace Controller: %w", err)
Expand All @@ -195,7 +195,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
s.Config.Manager,
opts,
kubebindv1alpha2.InformerScope(c.Options.ConsumerScope),
kubebindv1alpha2.Isolation(c.Options.ClusterScopedIsolation),
kubebindv1alpha2.Isolation(c.Options.Isolation),
c.Options.SchemaSource,
)
if err != nil {
Expand Down
74 changes: 72 additions & 2 deletions cli/pkg/kubectl/dev/plugin/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package plugin
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
Expand Down Expand Up @@ -67,6 +70,14 @@ type DevOptions struct {
KindNetwork string
}

// fallbackAssetVersion is used when unable to fetch the latest version
const fallbackAssetVersion = "0.6.0"

// gitHubRelease represents a GitHub release response
type gitHubRelease struct {
TagName string `json:"tag_name"`
}

// NewDevOptions creates a new DevOptions
func NewDevOptions(streams genericclioptions.IOStreams) *DevOptions {
opts := base.NewOptions(streams)
Expand All @@ -77,7 +88,7 @@ func NewDevOptions(streams genericclioptions.IOStreams) *DevOptions {
ProviderClusterName: "kind-provider",
ConsumerClusterName: "kind-consumer",
ChartPath: "oci://ghcr.io/kube-bind/charts/backend",
ChartVersion: "v0.6.0",
ChartVersion: fallbackAssetVersion,
}
}

Expand All @@ -92,15 +103,74 @@ func (o *DevOptions) AddCmdFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.ChartPath, "chart-path", o.ChartPath, "Helm chart path or OCI registry URL")
cmd.Flags().StringVar(&o.ChartVersion, "chart-version", o.ChartVersion, "Helm chart version")
cmd.Flags().StringVar(&o.Image, "image", "ghcr.io/kube-bind/backend", "kube-bind backend image to use in dev mode")
cmd.Flags().StringVar(&o.Tag, "tag", "main", "kube-bind backend image tag to use in dev mode")
cmd.Flags().StringVar(&o.Tag, "tag", "", "kube-bind backend image tag to use in dev mode")
cmd.Flags().StringVar(&o.KindNetwork, "kind-network", "kube-bind-dev", "kind network to use in dev mode")
}

// Complete completes the options
func (o *DevOptions) Complete(args []string) error {
// Only fetch the latest version if tag is not set
var assetVersion string
if o.Tag == "" {
version, err := fetchLatestRelease()
if err != nil {
// Log the error but continue with fallback version
fmt.Fprintf(o.Streams.ErrOut, "Warning: Failed to fetch latest release version: %v. Using fallback version %s\n", err, fallbackAssetVersion)
assetVersion = fallbackAssetVersion
} else {
assetVersion = version
}

// Update options with the resolved version
if o.ChartVersion == "" || o.ChartVersion == fallbackAssetVersion {
o.ChartVersion = assetVersion
}
if o.Tag == "" || o.Tag == "v"+fallbackAssetVersion {
o.Tag = "v" + assetVersion
}
}

return nil
}

// fetchLatestRelease fetches the latest release version from GitHub
func fetchLatestRelease() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/kube-bind/kube-bind/releases/latest", nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to fetch latest release: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}

var release gitHubRelease
if err := json.Unmarshal(body, &release); err != nil {
return "", fmt.Errorf("failed to parse release data: %w", err)
}

if release.TagName == "" {
return "", fmt.Errorf("no tag name in release data")
}

version := strings.TrimPrefix(release.TagName, "v")
return version, nil
}

// Validate validates the options
func (o *DevOptions) Validate() error {
return o.Options.Validate()
Expand Down
2 changes: 1 addition & 1 deletion contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ spec:
crd: {}
- group: kube-bind.io
name: apiserviceexports
schema: v251112-503d98b.apiserviceexports.kube-bind.io
schema: v260119-2e5a3e93.apiserviceexports.kube-bind.io
storage:
crd: {}
- group: kube-bind.io
Expand Down
Loading