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:
parent
4cbc26eaf4
commit
bc6f311ac6
|
@ -22,6 +22,8 @@ import (
|
|||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
|
@ -52,20 +54,26 @@ const (
|
|||
)
|
||||
|
||||
type App struct {
|
||||
Config config.EnvConfig
|
||||
Config *config.EnvConfig
|
||||
logger *slog.Logger
|
||||
repositories *repositories.Repositories
|
||||
Scheme *runtime.Scheme
|
||||
RequestAuthN authenticator.Request
|
||||
RequestAuthZ authorizer.Authorizer
|
||||
}
|
||||
|
||||
// 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{
|
||||
Config: cfg,
|
||||
logger: logger,
|
||||
repositories: repositories.NewRepositories(cl),
|
||||
Scheme: scheme,
|
||||
RequestAuthN: reqAuthN,
|
||||
RequestAuthZ: reqAuthZ,
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -46,6 +46,15 @@ func (a *App) LogError(r *http.Request, err error) {
|
|||
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
|
||||
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
httpError := &HTTPError{
|
||||
|
@ -103,6 +112,30 @@ func (a *App) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
|||
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
|
||||
func (a *App) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
|
||||
message, err := json.Marshal(errors)
|
||||
|
|
|
@ -26,28 +26,13 @@ import (
|
|||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
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 (
|
||||
a App
|
||||
)
|
||||
|
||||
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() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, HealthCheckPath, http.NoBody)
|
||||
|
|
|
@ -20,7 +20,9 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -28,6 +30,18 @@ type NamespacesEnvelope Envelope[[]models.Namespace]
|
|||
|
||||
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())
|
||||
if err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
|
|
|
@ -29,15 +29,10 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/namespaces"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
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.
|
||||
// therefore, we run them using the `Serial` Ginkgo decorators.
|
||||
|
@ -47,14 +42,6 @@ var _ = Describe("Namespaces Handler", func() {
|
|||
const namespaceName2 = "get-ns-test-ns2"
|
||||
|
||||
BeforeEach(func() {
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -95,6 +82,9 @@ var _ = Describe("Namespaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, AllNamespacesPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetNamespacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
|
|
|
@ -19,39 +19,52 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"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/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/rest"
|
||||
"k8s.io/utils/ptr"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"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
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
const (
|
||||
userIdHeader = "userid-header"
|
||||
userIdPrefix = ""
|
||||
groupsHeader = "groups-header"
|
||||
|
||||
adminUser = "notebooks-admin"
|
||||
)
|
||||
|
||||
var (
|
||||
testEnv *envtest.Environment
|
||||
cfg *rest.Config
|
||||
|
||||
k8sClient client.Client
|
||||
|
||||
a *App
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
@ -95,6 +108,30 @@ var _ = BeforeSuite(func() {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
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")
|
||||
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||
Scheme: scheme.Scheme,
|
||||
|
@ -104,6 +141,21 @@ var _ = BeforeSuite(func() {
|
|||
})
|
||||
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() {
|
||||
defer GinkgoRecover()
|
||||
err = k8sManager.Start(ctx)
|
||||
|
|
|
@ -22,7 +22,10 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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"
|
||||
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) {
|
||||
name := ps.ByName("name")
|
||||
|
||||
if name == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing"))
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
// =========================== 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())
|
||||
if err != nil {
|
||||
a.serverErrorResponse(w, r, err)
|
||||
|
|
|
@ -32,9 +32,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspacekinds"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
var _ = Describe("WorkspaceKinds Handler", func() {
|
||||
|
@ -47,8 +45,6 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
const namespaceName1 = "wsk-exist-test-ns1"
|
||||
|
||||
var (
|
||||
a App
|
||||
|
||||
workspaceKind1Name string
|
||||
workspaceKind1Key types.NamespacedName
|
||||
workspaceKind2Name string
|
||||
|
@ -62,14 +58,6 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
workspaceKind2Name = fmt.Sprintf("workspacekind-2-%s", uniqueName)
|
||||
workspaceKind2Key = types.NamespacedName{Name: workspaceKind2Name}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -111,7 +99,6 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(ctx, namespace1)).To(Succeed())
|
||||
|
||||
})
|
||||
|
||||
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)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspaceKindsHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
|
@ -164,6 +154,9 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspaceKindHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: WorkspaceKindNamePathParam, Value: workspaceKind1Name},
|
||||
|
@ -206,23 +199,14 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
// therefore, we run them using the `Serial` Ginkgo decorators.
|
||||
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() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, AllWorkspaceKindsPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
|
@ -254,6 +238,9 @@ var _ = Describe("WorkspaceKinds Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspaceKindHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: WorkspaceNamePathParam, Value: missingWorkspaceKindName},
|
||||
|
|
|
@ -23,7 +23,10 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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"
|
||||
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) {
|
||||
namespace := ps.ByName(NamespacePathParam)
|
||||
workspaceName := ps.ByName(WorkspaceNamePathParam)
|
||||
|
||||
var workspace models.Workspace
|
||||
var err error
|
||||
if namespace == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil"))
|
||||
return
|
||||
}
|
||||
|
||||
workspaceName := ps.ByName(WorkspaceNamePathParam)
|
||||
if workspaceName == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil"))
|
||||
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 errors.Is(err, repository.ErrWorkspaceNotFound) {
|
||||
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) {
|
||||
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 err error
|
||||
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) {
|
||||
namespace := ps.ByName("namespace")
|
||||
|
||||
if namespace == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
|
||||
return
|
||||
|
@ -109,6 +142,23 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
|
|||
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
namespace := ps.ByName("namespace")
|
||||
workspaceName := ps.ByName("name")
|
||||
|
||||
if namespace == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
workspaceName := ps.ByName("name")
|
||||
if workspaceName == "" {
|
||||
a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing"))
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrWorkspaceNotFound) {
|
||||
|
|
|
@ -34,9 +34,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/config"
|
||||
models "github.com/kubeflow/notebooks/workspaces/backend/internal/models/workspaces"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/repositories"
|
||||
)
|
||||
|
||||
var _ = Describe("Workspaces Handler", func() {
|
||||
|
@ -50,8 +48,6 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
const namespaceName2 = "ws-exist-ns2"
|
||||
|
||||
var (
|
||||
a App
|
||||
|
||||
workspaceName1 string
|
||||
workspaceKey1 types.NamespacedName
|
||||
workspaceName2 string
|
||||
|
@ -74,14 +70,6 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
|
||||
workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -173,6 +161,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
|
@ -225,6 +216,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
|
@ -277,6 +271,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspaceHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
|
@ -327,8 +324,6 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
const namespaceName1 = "ws-invalid-ns1"
|
||||
|
||||
var (
|
||||
a App
|
||||
|
||||
workspaceMissingWskName string
|
||||
workspaceMissingWskKey types.NamespacedName
|
||||
|
||||
|
@ -353,14 +348,6 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
|
||||
workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating Namespace 1")
|
||||
namespace1 := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -439,6 +426,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
|
@ -506,6 +496,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspaceHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: namespaceName1},
|
||||
|
@ -542,23 +535,14 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
// therefore, we run them using the `Serial` Ginkgo decorators.
|
||||
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() {
|
||||
By("creating the HTTP request")
|
||||
req, err := http.NewRequest(http.MethodGet, AllWorkspacesPath, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{}
|
||||
rr := httptest.NewRecorder()
|
||||
|
@ -590,6 +574,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspacesHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
|
||||
|
@ -625,6 +612,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing GetWorkspaceHandler")
|
||||
ps := httprouter.Params{
|
||||
httprouter.Param{Key: NamespacePathParam, Value: missingNamespace},
|
||||
|
@ -647,7 +637,6 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
const namespaceNameCrud = "ws-crud-ns"
|
||||
|
||||
var (
|
||||
a App
|
||||
workspaceName string
|
||||
workspaceKey types.NamespacedName
|
||||
workspaceKindName string
|
||||
|
@ -661,14 +650,6 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
workspaceKindName = fmt.Sprintf("workspacekind-%s", uniqueName)
|
||||
workspaceKindKey = types.NamespacedName{Name: workspaceKindName}
|
||||
|
||||
repos := repositories.NewRepositories(k8sClient)
|
||||
a = App{
|
||||
Config: config.EnvConfig{
|
||||
Port: 4000,
|
||||
},
|
||||
repositories: repos,
|
||||
}
|
||||
|
||||
By("creating the Namespace")
|
||||
namespaceA := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -755,6 +736,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing CreateWorkspaceHandler")
|
||||
rr := httptest.NewRecorder()
|
||||
ps := httprouter.Params{
|
||||
|
@ -783,6 +767,9 @@ var _ = Describe("Workspaces Handler", func() {
|
|||
req, err = http.NewRequest(http.MethodDelete, path, http.NoBody)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("setting the auth headers")
|
||||
req.Header.Set(userIdHeader, adminUser)
|
||||
|
||||
By("executing DeleteWorkspaceHandler")
|
||||
rr = httptest.NewRecorder()
|
||||
ps = httprouter.Params{
|
||||
|
|
|
@ -26,27 +26,78 @@ import (
|
|||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
|
||||
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/helper"
|
||||
"github.com/kubeflow/notebooks/workspaces/backend/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var cfg config.EnvConfig
|
||||
flag.IntVar(&cfg.Port, "port", getEnvAsInt("PORT", 4000), "API server port")
|
||||
// Define command line flags
|
||||
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))
|
||||
|
||||
kubeconfig, err := helper.GetKubeconfig()
|
||||
// Build the Kubernetes client configuration
|
||||
kubeconfig, err := ctrl.GetConfig()
|
||||
if err != nil {
|
||||
logger.Error("failed to get kubeconfig", "error", err)
|
||||
logger.Error("failed to get Kubernetes config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
kubeconfig.QPS = float32(cfg.ClientQPS)
|
||||
kubeconfig.Burst = cfg.ClientBurst
|
||||
|
||||
// Build the Kubernetes scheme
|
||||
scheme, err := helper.BuildScheme()
|
||||
if err != nil {
|
||||
logger.Error("failed to build Kubernetes scheme", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create the controller manager
|
||||
mgr, err := ctrl.NewManager(kubeconfig, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{
|
||||
|
@ -60,7 +111,21 @@ func main() {
|
|||
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 {
|
||||
logger.Error("failed to create app", "error", err)
|
||||
os.Exit(1)
|
||||
|
@ -75,6 +140,7 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start the controller manager
|
||||
logger.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
logger.Error("problem running manager", "error", err)
|
||||
|
@ -90,3 +156,28 @@ func getEnvAsInt(name string, defaultVal int) int {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -9,23 +9,29 @@ require (
|
|||
github.com/kubeflow/notebooks/workspaces/controller v0.0.0
|
||||
github.com/onsi/ginkgo/v2 v2.19.0
|
||||
github.com/onsi/gomega v1.33.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
k8s.io/api v0.31.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apiserver v0.31.0
|
||||
k8s.io/client-go v0.31.0
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
sigs.k8s.io/controller-runtime v0.19.1
|
||||
)
|
||||
|
||||
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/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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.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/fxamacker/cbor/v2 v2.7.0 // 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-openapi/jsonpointer v0.19.6 // 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // 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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // 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/stoewer/go-strcase v1.2.0 // 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/zap v1.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/net v0.26.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/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // 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/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/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
|
|
|
@ -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/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/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/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=
|
||||
|
@ -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/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/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/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/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/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/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
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/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
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/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/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/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/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/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
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.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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
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/yuin/goldmark v1.1.27/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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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-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.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-20190412213103-97732733099d/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=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
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/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
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/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
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/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/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/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
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/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -18,4 +18,13 @@ package config
|
|||
|
||||
type EnvConfig struct {
|
||||
Port int
|
||||
|
||||
ClientQPS float64
|
||||
ClientBurst int
|
||||
|
||||
DisableAuth bool
|
||||
|
||||
UserIdHeader string
|
||||
UserIdPrefix string
|
||||
GroupsHeader string
|
||||
}
|
||||
|
|
|
@ -19,22 +19,11 @@ package helper
|
|||
import (
|
||||
"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"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
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.
|
||||
func BuildScheme() (*runtime.Scheme, error) {
|
||||
scheme := runtime.NewScheme()
|
||||
|
|
Loading…
Reference in New Issue