feat(ws): add auth to backend (#202)

* feat(ws): add auth to backend

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* add `DISABLE_AUTH` for interim testing (enabled by default)

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

---------

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>
This commit is contained in:
Mathew Wicks 2025-02-11 12:21:28 -08:00 committed by GitHub
parent 4cbc26eaf4
commit bc6f311ac6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 725 additions and 134 deletions

View File

@ -22,6 +22,8 @@ import (
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authorization/authorizer"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config" "github.com/kubeflow/notebooks/workspaces/backend/internal/config"
@ -52,20 +54,26 @@ const (
) )
type App struct { type App struct {
Config config.EnvConfig Config *config.EnvConfig
logger *slog.Logger logger *slog.Logger
repositories *repositories.Repositories repositories *repositories.Repositories
Scheme *runtime.Scheme Scheme *runtime.Scheme
RequestAuthN authenticator.Request
RequestAuthZ authorizer.Authorizer
} }
// NewApp creates a new instance of the app // NewApp creates a new instance of the app
func NewApp(cfg config.EnvConfig, logger *slog.Logger, cl client.Client, scheme *runtime.Scheme) (*App, error) { func NewApp(cfg *config.EnvConfig, logger *slog.Logger, cl client.Client, scheme *runtime.Scheme, reqAuthN authenticator.Request, reqAuthZ authorizer.Authorizer) (*App, error) {
// TODO: log the configuration on startup
app := &App{ app := &App{
Config: cfg, Config: cfg,
logger: logger, logger: logger,
repositories: repositories.NewRepositories(cl), repositories: repositories.NewRepositories(cl),
Scheme: scheme, Scheme: scheme,
RequestAuthN: reqAuthN,
RequestAuthZ: reqAuthZ,
} }
return app, nil return app, nil
} }

View File

@ -0,0 +1,73 @@
/*
Copyright 2024.
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 api
import (
"fmt"
"net/http"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
)
// requireAuth verifies that the request is authenticated and authorized to take the actions specified by the given policies.
// If this method returns false, the request has been handled and the caller should return immediately.
// If this method returns true, the request is authenticated and authorized to proceed.
// This method should only be called once per request.
func (a *App) requireAuth(w http.ResponseWriter, r *http.Request, policies []*auth.ResourcePolicy) bool {
ctx := r.Context()
// if auth is disabled, allow the request to proceed
if a.Config.DisableAuth {
return true
}
// authenticate the request (extract user and groups from the request headers)
res, ok, err := a.RequestAuthN.AuthenticateRequest(r)
if err != nil {
err = fmt.Errorf("failed to authenticate request: %w", err)
a.serverErrorResponse(w, r, err)
return false
}
if !ok {
a.unauthorizedResponse(w, r)
return false
}
// for each policy, check if the user is authorized to take the requested action
for _, policy := range policies {
attributes := policy.AttributesFor(res.User)
authorized, reason, err := a.RequestAuthZ.Authorize(ctx, attributes)
if err != nil {
err = fmt.Errorf("failed to authorize request for user %q: %w", res.User.GetName(), err)
a.serverErrorResponse(w, r, err)
return false
}
if authorized != authorizer.DecisionAllow {
msg := fmt.Sprintf("authorization was denied for user %q", res.User.GetName())
if reason != "" {
msg = fmt.Sprintf("%s: %s", msg, reason)
}
a.forbiddenResponse(w, r, msg)
return false
}
}
return true
}

View File

@ -46,6 +46,15 @@ func (a *App) LogError(r *http.Request, err error) {
a.logger.Error(err.Error(), "method", method, "uri", uri) a.logger.Error(err.Error(), "method", method, "uri", uri)
} }
func (a *App) LogWarn(r *http.Request, message string) {
var (
method = r.Method
uri = r.URL.RequestURI()
)
a.logger.Warn(message, "method", method, "uri", uri)
}
//nolint:unused //nolint:unused
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{ httpError := &HTTPError{
@ -103,6 +112,30 @@ func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
a.errorResponse(w, r, httpError) a.errorResponse(w, r, httpError)
} }
func (a *App) unauthorizedResponse(w http.ResponseWriter, r *http.Request) {
httpError := &HTTPError{
StatusCode: http.StatusUnauthorized,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusUnauthorized),
Message: "authentication is required to access this resource",
},
}
a.errorResponse(w, r, httpError)
}
func (a *App) forbiddenResponse(w http.ResponseWriter, r *http.Request, msg string) {
a.LogWarn(r, msg)
httpError := &HTTPError{
StatusCode: http.StatusForbidden,
ErrorResponse: ErrorResponse{
Code: strconv.Itoa(http.StatusForbidden),
Message: "you are not authorized to access this resource",
},
}
a.errorResponse(w, r, httpError)
}
//nolint:unused //nolint:unused
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) { func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
message, err := json.Marshal(errors) message, err := json.Marshal(errors)

View File

@ -26,28 +26,13 @@ import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/health_check" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/health_check"
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
) )
var _ = Describe("HealthCheck Handler", func() { var _ = Describe("HealthCheck Handler", func() {
var (
a App
)
Context("when backend is healthy", func() { Context("when backend is healthy", func() {
BeforeEach(func() {
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
})
It("should return a health check response", func() { It("should return a health check response", func() {
By("creating the HTTP request") By("creating the HTTP request")
req, err := http.NewRequest(http.MethodGet, HealthCheckPath, http.NoBody) req, err := http.NewRequest(http.MethodGet, HealthCheckPath, http.NoBody)

View File

@ -20,7 +20,9 @@ import (
"net/http" "net/http"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
corev1 "k8s.io/api/core/v1"
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
) )
@ -28,6 +30,18 @@ type NamespacesEnvelope Envelope[[]models.Namespace]
func (a *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func (a *App) GetNamespacesHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbList,
&corev1.Namespace{},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
namespaces, err := a.repositories.Namespace.GetNamespaces(r.Context()) namespaces, err := a.repositories.Namespace.GetNamespaces(r.Context())
if err != nil { if err != nil {
a.serverErrorResponse(w, r, err) a.serverErrorResponse(w, r, err)

View File

@ -29,15 +29,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
) )
var _ = Describe("Namespaces Handler", func() { var _ = Describe("Namespaces Handler", func() {
var (
a App
)
// NOTE: these tests assume a specific state of the cluster, so cannot be run in parallel with other tests. // NOTE: these tests assume a specific state of the cluster, so cannot be run in parallel with other tests.
// therefore, we run them using the `Serial` Ginkgo decorators. // therefore, we run them using the `Serial` Ginkgo decorators.
@ -47,14 +42,6 @@ var _ = Describe("Namespaces Handler", func() {
const namespaceName2 = "get-ns-test-ns2" const namespaceName2 = "get-ns-test-ns2"
BeforeEach(func() { BeforeEach(func() {
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
By("creating Namespace 1") By("creating Namespace 1")
namespace1 := &corev1.Namespace{ namespace1 := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -95,6 +82,9 @@ var _ = Describe("Namespaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, AllNamespacesPath, http.NoBody) req, err := http.NewRequest(http.MethodGet, AllNamespacesPath, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetNamespacesHandler") By("executing GetNamespacesHandler")
ps := httprouter.Params{} ps := httprouter.Params{}
rr := httptest.NewRecorder() rr := httptest.NewRecorder()

View File

@ -19,39 +19,52 @@ package api
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" "github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
) )
// These tests use Ginkgo (BDD-style Go testing framework). Refer to // These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. // http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
const (
userIdHeader = "userid-header"
userIdPrefix = ""
groupsHeader = "groups-header"
adminUser = "notebooks-admin"
)
var ( var (
testEnv *envtest.Environment testEnv *envtest.Environment
cfg *rest.Config cfg *rest.Config
k8sClient client.Client k8sClient client.Client
a *App
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
) )
@ -95,6 +108,30 @@ var _ = BeforeSuite(func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil()) Expect(k8sClient).NotTo(BeNil())
By("creating the notebooks-admin ClusterRoleBinding")
Expect(k8sClient.Create(ctx, &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "notebooks-admin",
},
Subjects: []rbacv1.Subject{
{
Kind: "User",
Name: adminUser,
},
},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: "cluster-admin",
},
})).To(Succeed())
By("listing the clusterRoles")
clusterRoles := &rbacv1.ClusterRoleList{}
Expect(k8sClient.List(ctx, clusterRoles)).To(Succeed())
for _, clusterRole := range clusterRoles.Items {
fmt.Printf("ClusterRole: %s\n", clusterRole.Name)
}
By("setting up the controller manager") By("setting up the controller manager")
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme, Scheme: scheme.Scheme,
@ -104,6 +141,21 @@ var _ = BeforeSuite(func() {
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("initializing the application logger")
appLogger := slog.New(slog.NewTextHandler(GinkgoWriter, nil))
By("creating the request authenticator")
reqAuthN, err := auth.NewRequestAuthenticator(userIdHeader, userIdPrefix, groupsHeader)
Expect(err).NotTo(HaveOccurred())
By("creating the request authorizer")
reqAuthZ, err := auth.NewRequestAuthorizer(k8sManager.GetConfig(), k8sManager.GetHTTPClient())
Expect(err).NotTo(HaveOccurred())
By("creating the application")
// NOTE: we use the `k8sClient` rather than `k8sManager.GetClient()` to avoid race conditions with the cached client
a, err = NewApp(&config.EnvConfig{}, appLogger, k8sClient, k8sManager.GetScheme(), reqAuthN, reqAuthZ)
go func() { go func() {
defer GinkgoRecover() defer GinkgoRecover()
err = k8sManager.Start(ctx) err = k8sManager.Start(ctx)

View File

@ -22,7 +22,10 @@ import (
"net/http" "net/http"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds"
repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspacekinds" repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspacekinds"
) )
@ -33,12 +36,25 @@ type WorkspaceKindEnvelope Envelope[models.WorkspaceKind]
func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
name := ps.ByName("name") name := ps.ByName("name")
if name == "" { if name == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing")) a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing"))
return return
} }
// =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbGet,
&kubefloworgv1beta1.WorkspaceKind{
ObjectMeta: metav1.ObjectMeta{Name: name},
},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
workspaceKind, err := a.repositories.WorkspaceKind.GetWorkspaceKind(r.Context(), name) workspaceKind, err := a.repositories.WorkspaceKind.GetWorkspaceKind(r.Context(), name)
if err != nil { if err != nil {
if errors.Is(err, repository.ErrWorkspaceKindNotFound) { if errors.Is(err, repository.ErrWorkspaceKindNotFound) {
@ -60,6 +76,18 @@ func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps
} }
func (a *App) GetWorkspaceKindsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func (a *App) GetWorkspaceKindsHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbList,
&kubefloworgv1beta1.WorkspaceKind{},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
workspaceKinds, err := a.repositories.WorkspaceKind.GetWorkspaceKinds(r.Context()) workspaceKinds, err := a.repositories.WorkspaceKind.GetWorkspaceKinds(r.Context())
if err != nil { if err != nil {
a.serverErrorResponse(w, r, err) a.serverErrorResponse(w, r, err)

View File

@ -32,9 +32,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds"
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
) )
var _ = Describe("WorkspaceKinds Handler", func() { var _ = Describe("WorkspaceKinds Handler", func() {
@ -47,8 +45,6 @@ var _ = Describe("WorkspaceKinds Handler", func() {
const namespaceName1 = "wsk-exist-test-ns1" const namespaceName1 = "wsk-exist-test-ns1"
var ( var (
a App
workspaceKind1Name string workspaceKind1Name string
workspaceKind1Key types.NamespacedName workspaceKind1Key types.NamespacedName
workspaceKind2Name string workspaceKind2Name string
@ -62,14 +58,6 @@ var _ = Describe("WorkspaceKinds Handler", func() {
workspaceKind2Name = fmt.Sprintf("workspacekind-2-%s", uniqueName) workspaceKind2Name = fmt.Sprintf("workspacekind-2-%s", uniqueName)
workspaceKind2Key = types.NamespacedName{Name: workspaceKind2Name} workspaceKind2Key = types.NamespacedName{Name: workspaceKind2Name}
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
By("creating Namespace 1") By("creating Namespace 1")
namespace1 := &corev1.Namespace{ namespace1 := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -111,7 +99,6 @@ var _ = Describe("WorkspaceKinds Handler", func() {
}, },
} }
Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed()) Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed())
}) })
It("should retrieve the all WorkspaceKinds successfully", func() { It("should retrieve the all WorkspaceKinds successfully", func() {
@ -119,6 +106,9 @@ var _ = Describe("WorkspaceKinds Handler", func() {
req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody) req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspaceKindsHandler") By("executing GetWorkspaceKindsHandler")
ps := httprouter.Params{} ps := httprouter.Params{}
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -164,6 +154,9 @@ var _ = Describe("WorkspaceKinds Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspaceKindHandler") By("executing GetWorkspaceKindHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: WorkspaceKindNamePathParam, Value: workspaceKind1Name}, httprouter.Param{Key: WorkspaceKindNamePathParam, Value: workspaceKind1Name},
@ -206,23 +199,14 @@ var _ = Describe("WorkspaceKinds Handler", func() {
// therefore, we run them using the `Serial` Ginkgo decorators. // therefore, we run them using the `Serial` Ginkgo decorators.
Context("with no existing WorkspaceKinds", Serial, func() { Context("with no existing WorkspaceKinds", Serial, func() {
var a App
BeforeEach(func() {
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
})
It("should return an empty list of WorkspaceKinds", func() { It("should return an empty list of WorkspaceKinds", func() {
By("creating the HTTP request") By("creating the HTTP request")
req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody) req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspacesHandler") By("executing GetWorkspacesHandler")
ps := httprouter.Params{} ps := httprouter.Params{}
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -254,6 +238,9 @@ var _ = Describe("WorkspaceKinds Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspaceKindHandler") By("executing GetWorkspaceKindHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: WorkspaceNamePathParam, Value: missingWorkspaceKindName}, httprouter.Param{Key: WorkspaceNamePathParam, Value: missingWorkspaceKindName},

View File

@ -23,7 +23,10 @@ import (
"net/http" "net/http"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces" repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspaces"
) )
@ -34,20 +37,35 @@ type WorkspaceEnvelope Envelope[models.Workspace]
func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName(NamespacePathParam) namespace := ps.ByName(NamespacePathParam)
workspaceName := ps.ByName(WorkspaceNamePathParam)
var workspace models.Workspace
var err error
if namespace == "" { if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil")) a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil"))
return return
} }
workspaceName := ps.ByName(WorkspaceNamePathParam)
if workspaceName == "" { if workspaceName == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil")) a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil"))
return return
} }
workspace, err = a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName) // =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbGet,
&kubefloworgv1beta1.Workspace{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: workspaceName,
},
},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
workspace, err := a.repositories.Workspace.GetWorkspace(r.Context(), namespace, workspaceName)
if err != nil { if err != nil {
if errors.Is(err, repository.ErrWorkspaceNotFound) { if errors.Is(err, repository.ErrWorkspaceNotFound) {
a.notFoundResponse(w, r) a.notFoundResponse(w, r)
@ -71,6 +89,22 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt
func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName(NamespacePathParam) namespace := ps.ByName(NamespacePathParam)
// =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbList,
&kubefloworgv1beta1.Workspace{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
},
},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
var workspaces []models.Workspace var workspaces []models.Workspace
var err error var err error
if namespace == "" { if namespace == "" {
@ -95,7 +129,6 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht
func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName("namespace") namespace := ps.ByName("namespace")
if namespace == "" { if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing")) a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
return return
@ -109,6 +142,23 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
workspaceModel.Namespace = namespace workspaceModel.Namespace = namespace
// =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbCreate,
&kubefloworgv1beta1.Workspace{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: workspaceModel.Name,
},
},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
createdWorkspace, err := a.repositories.Workspace.CreateWorkspace(r.Context(), workspaceModel) createdWorkspace, err := a.repositories.Workspace.CreateWorkspace(r.Context(), workspaceModel)
if err != nil { if err != nil {
a.serverErrorResponse(w, r, fmt.Errorf("error creating workspace: %w", err)) a.serverErrorResponse(w, r, fmt.Errorf("error creating workspace: %w", err))
@ -129,18 +179,34 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
func (a *App) DeleteWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func (a *App) DeleteWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName("namespace") namespace := ps.ByName("namespace")
workspaceName := ps.ByName("name")
if namespace == "" { if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing")) a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
return return
} }
workspaceName := ps.ByName("name")
if workspaceName == "" { if workspaceName == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing")) a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing"))
return return
} }
// =========================== AUTH ===========================
authPolicies := []*auth.ResourcePolicy{
auth.NewResourcePolicy(
auth.ResourceVerbDelete,
&kubefloworgv1beta1.Workspace{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: workspaceName,
},
},
),
}
if success := a.requireAuth(w, r, authPolicies); !success {
return
}
// ============================================================
err := a.repositories.Workspace.DeleteWorkspace(r.Context(), namespace, workspaceName) err := a.repositories.Workspace.DeleteWorkspace(r.Context(), namespace, workspaceName)
if err != nil { if err != nil {
if errors.Is(err, repository.ErrWorkspaceNotFound) { if errors.Is(err, repository.ErrWorkspaceNotFound) {

View File

@ -34,9 +34,7 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces" models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
) )
var _ = Describe("Workspaces Handler", func() { var _ = Describe("Workspaces Handler", func() {
@ -50,8 +48,6 @@ var _ = Describe("Workspaces Handler", func() {
const namespaceName2 = "ws-exist-ns2" const namespaceName2 = "ws-exist-ns2"
var ( var (
a App
workspaceName1 string workspaceName1 string
workspaceKey1 types.NamespacedName workspaceKey1 types.NamespacedName
workspaceName2 string workspaceName2 string
@ -74,14 +70,6 @@ var _ = Describe("Workspaces Handler", func() {
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
workspaceKindKey = types.NamespacedName{Name: workspaceKindName} workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
By("creating Namespace 1") By("creating Namespace 1")
namespace1 := &corev1.Namespace{ namespace1 := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -173,6 +161,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody) req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspacesHandler") By("executing GetWorkspacesHandler")
ps := httprouter.Params{} ps := httprouter.Params{}
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -225,6 +216,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspacesHandler") By("executing GetWorkspacesHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
@ -277,6 +271,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspaceHandler") By("executing GetWorkspaceHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
@ -327,8 +324,6 @@ var _ = Describe("Workspaces Handler", func() {
const namespaceName1 = "ws-invalid-ns1" const namespaceName1 = "ws-invalid-ns1"
var ( var (
a App
workspaceMissingWskName string workspaceMissingWskName string
workspaceMissingWskKey types.NamespacedName workspaceMissingWskKey types.NamespacedName
@ -353,14 +348,6 @@ var _ = Describe("Workspaces Handler", func() {
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
workspaceKindKey = types.NamespacedName{Name: workspaceKindName} workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
By("creating Namespace 1") By("creating Namespace 1")
namespace1 := &corev1.Namespace{ namespace1 := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -439,6 +426,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspacesHandler") By("executing GetWorkspacesHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
@ -506,6 +496,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspaceHandler") By("executing GetWorkspaceHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1}, httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
@ -542,23 +535,14 @@ var _ = Describe("Workspaces Handler", func() {
// therefore, we run them using the `Serial` Ginkgo decorators. // therefore, we run them using the `Serial` Ginkgo decorators.
Context("with no existing Workspaces", Serial, func() { Context("with no existing Workspaces", Serial, func() {
var a App
BeforeEach(func() {
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
})
It("should return an empty list of Workspaces for all namespaces", func() { It("should return an empty list of Workspaces for all namespaces", func() {
By("creating the HTTP request") By("creating the HTTP request")
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody) req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspacesHandler") By("executing GetWorkspacesHandler")
ps := httprouter.Params{} ps := httprouter.Params{}
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -590,6 +574,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspacesHandler") By("executing GetWorkspacesHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace}, httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
@ -625,6 +612,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err := http.NewRequest(http.MethodGet, path, http.NoBody) req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing GetWorkspaceHandler") By("executing GetWorkspaceHandler")
ps := httprouter.Params{ ps := httprouter.Params{
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace}, httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
@ -647,7 +637,6 @@ var _ = Describe("Workspaces Handler", func() {
const namespaceNameCrud = "ws-crud-ns" const namespaceNameCrud = "ws-crud-ns"
var ( var (
a App
workspaceName string workspaceName string
workspaceKey types.NamespacedName workspaceKey types.NamespacedName
workspaceKindName string workspaceKindName string
@ -661,14 +650,6 @@ var _ = Describe("Workspaces Handler", func() {
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName) workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
workspaceKindKey = types.NamespacedName{Name: workspaceKindName} workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
By("creating the Namespace") By("creating the Namespace")
namespaceA := &corev1.Namespace{ namespaceA := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -755,6 +736,9 @@ var _ = Describe("Workspaces Handler", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing CreateWorkspaceHandler") By("executing CreateWorkspaceHandler")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
ps := httprouter.Params{ ps := httprouter.Params{
@ -783,6 +767,9 @@ var _ = Describe("Workspaces Handler", func() {
req, err = http.NewRequest(http.MethodDelete, path, http.NoBody) req, err = http.NewRequest(http.MethodDelete, path, http.NoBody)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("setting the auth headers")
req.Header.Set(userIdHeader, adminUser)
By("executing DeleteWorkspaceHandler") By("executing DeleteWorkspaceHandler")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
ps = httprouter.Params{ ps = httprouter.Params{

View File

@ -26,27 +26,78 @@ import (
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
application "github.com/kubeflow/notebooks/workspaces/backend/api" application "github.com/kubeflow/notebooks/workspaces/backend/api"
"github.com/kubeflow/notebooks/workspaces/backend/internal/auth"
"github.com/kubeflow/notebooks/workspaces/backend/internal/config" "github.com/kubeflow/notebooks/workspaces/backend/internal/config"
"github.com/kubeflow/notebooks/workspaces/backend/internal/helper" "github.com/kubeflow/notebooks/workspaces/backend/internal/helper"
"github.com/kubeflow/notebooks/workspaces/backend/internal/server" "github.com/kubeflow/notebooks/workspaces/backend/internal/server"
) )
func main() { func main() {
var cfg config.EnvConfig // Define command line flags
flag.IntVar(&cfg.Port, "port", getEnvAsInt("PORT", 4000), "API server port") cfg := &config.EnvConfig{}
flag.IntVar(&cfg.Port,
"port",
getEnvAsInt("PORT", 4000),
"API server port",
)
flag.Float64Var(
&cfg.ClientQPS,
"client-qps",
getEnvAsFloat64("CLIENT_QPS", 50),
"QPS configuration passed to rest.Client",
)
flag.IntVar(
&cfg.ClientBurst,
"client-burst",
getEnvAsInt("CLIENT_BURST", 100),
"Maximum Burst configuration passed to rest.Client",
)
flag.BoolVar(
// TODO: remove before GA
&cfg.DisableAuth,
"disable-auth",
getEnvAsBool("DISABLE_AUTH", true),
"Disable authentication and authorization",
)
flag.StringVar(
&cfg.UserIdHeader,
"userid-header",
getEnvAsStr("USERID_HEADER", "kubeflow-userid"),
"Key of request header containing user id",
)
flag.StringVar(
&cfg.UserIdPrefix,
"userid-prefix",
getEnvAsStr("USERID_PREFIX", ":"),
"Request header user id common prefix",
)
flag.StringVar(
&cfg.GroupsHeader,
"groups-header",
getEnvAsStr("GROUPS_HEADER", "kubeflow-groups"),
"Key of request header containing user groups",
)
// Initialize the logger
logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
kubeconfig, err := helper.GetKubeconfig() // Build the Kubernetes client configuration
kubeconfig, err := ctrl.GetConfig()
if err != nil { if err != nil {
logger.Error("failed to get kubeconfig", "error", err) logger.Error("failed to get Kubernetes config", "error", err)
os.Exit(1) os.Exit(1)
} }
kubeconfig.QPS = float32(cfg.ClientQPS)
kubeconfig.Burst = cfg.ClientBurst
// Build the Kubernetes scheme
scheme, err := helper.BuildScheme() scheme, err := helper.BuildScheme()
if err != nil { if err != nil {
logger.Error("failed to build Kubernetes scheme", "error", err) logger.Error("failed to build Kubernetes scheme", "error", err)
os.Exit(1) os.Exit(1)
} }
// Create the controller manager
mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{ mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{
Scheme: scheme, Scheme: scheme,
Metrics: metricsserver.Options{ Metrics: metricsserver.Options{
@ -60,7 +111,21 @@ func main() {
os.Exit(1) os.Exit(1)
} }
app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme()) // Create the request authenticator
reqAuthN, err := auth.NewRequestAuthenticator(cfg.UserIdHeader, cfg.UserIdPrefix, cfg.GroupsHeader)
if err != nil {
logger.Error("failed to create request authenticator", "error", err)
os.Exit(1)
}
// Create the request authorizer
reqAuthZ, err := auth.NewRequestAuthorizer(mgr.GetConfig(), mgr.GetHTTPClient())
if err != nil {
logger.Error("failed to create request authorizer", "error", err)
}
// Create the application and server
app, err := application.NewApp(cfg, logger, mgr.GetClient(), mgr.GetScheme(), reqAuthN, reqAuthZ)
if err != nil { if err != nil {
logger.Error("failed to create app", "error", err) logger.Error("failed to create app", "error", err)
os.Exit(1) os.Exit(1)
@ -75,6 +140,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// Start the controller manager
logger.Info("starting manager") logger.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
logger.Error("problem running manager", "error", err) logger.Error("problem running manager", "error", err)
@ -90,3 +156,28 @@ func getEnvAsInt(name string, defaultVal int) int {
} }
return defaultVal return defaultVal
} }
func getEnvAsFloat64(name string, defaultVal float64) float64 {
if value, exists := os.LookupEnv(name); exists {
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
return floatValue
}
}
return defaultVal
}
func getEnvAsStr(name string, defaultVal string) string {
if value, exists := os.LookupEnv(name); exists {
return value
}
return defaultVal
}
func getEnvAsBool(name string, defaultVal bool) bool {
if value, exists := os.LookupEnv(name); exists {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}
}
return defaultVal
}

View File

@ -9,23 +9,29 @@ require (
github.com/kubeflow/notebooks/workspaces/controller v0.0.0 github.com/kubeflow/notebooks/workspaces/controller v0.0.0
github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1 github.com/onsi/gomega v1.33.1
github.com/stretchr/testify v1.9.0
k8s.io/api v0.31.0 k8s.io/api v0.31.0
k8s.io/apimachinery v0.31.0 k8s.io/apimachinery v0.31.0
k8s.io/apiserver v0.31.0
k8s.io/client-go v0.31.0 k8s.io/client-go v0.31.0
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
sigs.k8s.io/controller-runtime v0.19.1 sigs.k8s.io/controller-runtime v0.19.1
) )
require ( require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
@ -34,12 +40,15 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
@ -47,31 +56,46 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/net v0.26.0 // indirect golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect
k8s.io/component-base v0.31.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect

View File

@ -1,7 +1,16 @@
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -13,12 +22,17 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
@ -36,6 +50,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -48,8 +64,12 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -93,12 +113,18 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -108,6 +134,22 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@ -132,6 +174,8 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -157,6 +201,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -166,6 +216,7 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@ -178,14 +229,20 @@ k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24
k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk=
sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=

View File

@ -0,0 +1,70 @@
/*
Copyright 2024.
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 auth
import (
"fmt"
"net/http"
"strings"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/authentication/user"
)
// NewRequestAuthenticator returns a new request authenticator based on the provided configuration.
func NewRequestAuthenticator(useridHeader string, useridPrefix string, groupsHeader string) (authenticator.Request, error) {
// create an upstream `requestHeaderAuthRequestHandler` to extract user and groups from the request headers
requestHeaderAuthenticator, err := headerrequest.New(
[]string{useridHeader},
[]string{groupsHeader},
nil,
)
if err != nil {
return nil, fmt.Errorf("failed to create request header authenticator: %w", err)
}
// if the user id prefix is empty, return the upstream authenticator as is
if useridPrefix == "" {
return requestHeaderAuthenticator, nil
}
// wrap the upstream authenticator to trim the user prefix from the user id
requestAuthenticator := authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
response, ok, err := requestHeaderAuthenticator.AuthenticateRequest(req)
if err != nil {
return nil, false, err
}
// if the request was not authenticated, return the response as is
if !ok {
return response, ok, nil
}
// trim the user id prefix from the username
return &authenticator.Response{
User: &user.DefaultInfo{
Name: strings.TrimPrefix(response.User.GetName(), useridPrefix),
Groups: response.User.GetGroups(),
Extra: response.User.GetExtra(),
},
}, true, nil
})
return requestAuthenticator, nil
}

View File

@ -0,0 +1,128 @@
/*
Copyright 2024.
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 auth
import (
"fmt"
"net/http"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
allowCacheTTL = 10 * time.Second
denyCacheTTL = 10 * time.Second
)
// NewRequestAuthorizer returns a new request authorizer based on the provided configuration.
// loosely based on `WithAuthenticationAndAuthorization` from: https://github.com/kubernetes-sigs/controller-runtime/blob/v0.20.1/pkg/metrics/filters/filters.go#L36-L122
func NewRequestAuthorizer(restConfig *rest.Config, httpClient *http.Client) (authorizer.Authorizer, error) {
authorizationV1Client, err := authorizationv1.NewForConfigAndClient(restConfig, httpClient)
if err != nil {
return nil, err
}
authorizerConfig := authorizerfactory.DelegatingAuthorizerConfig{
SubjectAccessReviewClient: authorizationV1Client,
// AllowCacheTTL is the length of time that a successful authorization response will be cached
AllowCacheTTL: allowCacheTTL,
// DenyCacheTTL is the length of time that a denied authorization response will be cached
DenyCacheTTL: denyCacheTTL,
// wait.Backoff is copied from: https://github.com/kubernetes/apiserver/blob/v0.29.0/pkg/server/options/authentication.go#L43-L50
// options.DefaultAuthWebhookRetryBackoff is not used to avoid a dependency on "k8s.io/apiserver/pkg/server/options".
WebhookRetryBackoff: &wait.Backoff{
Duration: 500 * time.Millisecond,
Factor: 1.5,
Jitter: 0.2,
Steps: 5,
},
}
delegatingAuthorizer, err := authorizerConfig.New()
if err != nil {
return nil, fmt.Errorf("failed to create authorizer: %w", err)
}
return delegatingAuthorizer, nil
}
type ResourcePolicy struct {
Verb ResourceVerb
Group string
Version string
Kind string
Namespace string
Name string
}
// NewResourcePolicy returns a new resource policy based on the provided verb and resource object.
func NewResourcePolicy(verb ResourceVerb, object client.Object) *ResourcePolicy {
policy := &ResourcePolicy{
Verb: verb,
Group: object.GetObjectKind().GroupVersionKind().Group,
Version: object.GetObjectKind().GroupVersionKind().Version,
Kind: object.GetObjectKind().GroupVersionKind().Kind,
}
if object.GetNamespace() != "" {
policy.Namespace = object.GetNamespace()
}
if object.GetName() != "" {
policy.Name = object.GetName()
}
return policy
}
// AttributesFor returns an authorizer.Attributes which could be used with an authorizer.Authorizer to authorize the user for the resource policy.
func (p *ResourcePolicy) AttributesFor(u user.Info) authorizer.Attributes {
return authorizer.AttributesRecord{
User: u,
Verb: string(p.Verb),
Namespace: p.Namespace,
APIGroup: p.Group,
APIVersion: p.Version,
Resource: p.Kind,
Name: p.Name,
ResourceRequest: true,
}
}
// ResourceVerb represents a verb for an action on a resource.
type ResourceVerb string
const (
ResourceVerbCreate ResourceVerb = "create"
ResourceVerbGet ResourceVerb = "get"
ResourceVerbList ResourceVerb = "list"
ResourceVerbUpdate ResourceVerb = "update"
ResourceVerbPatch ResourceVerb = "patch"
ResourceVerbDelete ResourceVerb = "delete"
)

View File

@ -18,4 +18,13 @@ package config
type EnvConfig struct { type EnvConfig struct {
Port int Port int
ClientQPS float64
ClientBurst int
DisableAuth bool
UserIdHeader string
UserIdPrefix string
GroupsHeader string
} }

View File

@ -19,22 +19,11 @@ package helper
import ( import (
"fmt" "fmt"
"k8s.io/apimachinery/pkg/runtime"
clientRest "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme" clientgoscheme "k8s.io/client-go/kubernetes/scheme"
) )
// GetKubeconfig returns the current KUBECONFIG configuration based on the default loading rules.
func GetKubeconfig() (*clientRest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
return kubeConfig.ClientConfig()
}
// BuildScheme returns builds a new runtime scheme with all the necessary types registered. // BuildScheme returns builds a new runtime scheme with all the necessary types registered.
func BuildScheme() (*runtime.Scheme, error) { func BuildScheme() (*runtime.Scheme, error) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()