1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-17 15:58:41 +00:00

Merge branch 'main' into fix/proxy-store-update-add-gvk

This commit is contained in:
gehrkefc
2024-09-09 13:40:25 -03:00
7 changed files with 810 additions and 55 deletions

4
go.mod
View File

@@ -2,7 +2,7 @@ module github.com/rancher/steve
go 1.22.0 go 1.22.0
toolchain go1.22.2 toolchain go1.22.7
replace ( replace (
github.com/crewjam/saml => github.com/rancher/saml v0.2.0 github.com/crewjam/saml => github.com/rancher/saml v0.2.0
@@ -20,7 +20,7 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_golang v1.16.0
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146 github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
github.com/rancher/dynamiclistener v0.6.0 github.com/rancher/dynamiclistener v0.6.1-rc.1
github.com/rancher/kubernetes-provider-detector v0.1.5 github.com/rancher/kubernetes-provider-detector v0.1.5
github.com/rancher/lasso v0.0.0-20240828170735-d79536cac289 github.com/rancher/lasso v0.0.0-20240828170735-d79536cac289
github.com/rancher/norman v0.0.0-20240822182819-60ccfabc4ac5 github.com/rancher/norman v0.0.0-20240822182819-60ccfabc4ac5

4
go.sum
View File

@@ -189,8 +189,8 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146 h1:6I4Z7PAGmned9+EYxbMS7kvajId3r8+ZwAR5wB7X3kg= github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146 h1:6I4Z7PAGmned9+EYxbMS7kvajId3r8+ZwAR5wB7X3kg=
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146/go.mod h1:ZNk+LcRGwQYHqgbsJijRrI49KFbX31/QzoUBq4rAeV0= github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146/go.mod h1:ZNk+LcRGwQYHqgbsJijRrI49KFbX31/QzoUBq4rAeV0=
github.com/rancher/dynamiclistener v0.6.0 h1:M7x8Nq+GY0UORULANuW/AH1ocnyZaqlmTuviMQAHL1Q= github.com/rancher/dynamiclistener v0.6.1-rc.1 h1:EGmTpPzSI5LHj35Wg3NHFmKSbZdzNuh5zptws1jo/yo=
github.com/rancher/dynamiclistener v0.6.0/go.mod h1:7VNEQhAwzbYJ08S1MYb6B4vili6K7CcrG4cNZXq1j+s= github.com/rancher/dynamiclistener v0.6.1-rc.1/go.mod h1:7VNEQhAwzbYJ08S1MYb6B4vili6K7CcrG4cNZXq1j+s=
github.com/rancher/kubernetes-provider-detector v0.1.5 h1:hWRAsWuJOemzGjz/XrbTlM7QmfO4OedvFE3QwXiH60I= github.com/rancher/kubernetes-provider-detector v0.1.5 h1:hWRAsWuJOemzGjz/XrbTlM7QmfO4OedvFE3QwXiH60I=
github.com/rancher/kubernetes-provider-detector v0.1.5/go.mod h1:ypuJS7kP7rUiAn330xG46mj+Nhvym05GM8NqMVekpH0= github.com/rancher/kubernetes-provider-detector v0.1.5/go.mod h1:ypuJS7kP7rUiAn330xG46mj+Nhvym05GM8NqMVekpH0=
github.com/rancher/lasso v0.0.0-20240828170735-d79536cac289 h1:gbV7qLOcEgyTgep2ocl8FFhfGOUMQuvfV5OIIENHWT4= github.com/rancher/lasso v0.0.0-20240828170735-d79536cac289 h1:gbV7qLOcEgyTgep2ocl8FFhfGOUMQuvfV5OIIENHWT4=

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"os" "os"
"github.com/rancher/dynamiclistener/server"
"github.com/rancher/steve/pkg/debug" "github.com/rancher/steve/pkg/debug"
stevecli "github.com/rancher/steve/pkg/server/cli" stevecli "github.com/rancher/steve/pkg/server/cli"
"github.com/rancher/steve/pkg/version" "github.com/rancher/steve/pkg/version"
@@ -38,5 +39,5 @@ func run(_ *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
return s.ListenAndServe(ctx, config.HTTPSListenPort, config.HTTPListenPort, nil) return s.ListenAndServe(ctx, config.HTTPSListenPort, config.HTTPListenPort, &server.ListenOpts{DisplayServerLogs: true})
} }

View File

@@ -13,15 +13,6 @@ import (
"strconv" "strconv"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/partition"
"github.com/rancher/wrangler/v3/pkg/data"
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"github.com/rancher/wrangler/v3/pkg/summary"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@@ -34,9 +25,24 @@ import (
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/wrangler/v3/pkg/data"
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"github.com/rancher/wrangler/v3/pkg/summary"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/partition"
) )
const watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS" const (
watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
errNamespaceRequired = "metadata.namespace is required"
)
var ( var (
lowerChars = regexp.MustCompile("[a-z]+") lowerChars = regexp.MustCompile("[a-z]+")
@@ -422,13 +428,20 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params
} }
name := types.Name(input) name := types.Name(input)
ns := types.Namespace(input) namespace := types.Namespace(input)
if name == "" && input.String("metadata", "generateName") == "" { generateName := input.String("metadata", "generateName")
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
if name == "" && generateName == "" {
input.SetNested(schema.ID[0:1]+"-", "metadata", "generateName")
} }
if ns == "" && apiOp.Namespace != "" {
ns = apiOp.Namespace if attributes.Namespaced(schema) && namespace == "" {
input.SetNested(ns, "metadata", "namespace") if apiOp.Namespace == "" {
return nil, nil, apierror.NewAPIError(validation.InvalidBodyContent, errNamespaceRequired)
}
namespace = apiOp.Namespace
input.SetNested(namespace, "metadata", "namespace")
} }
gvk := attributes.GVK(schema) gvk := attributes.GVK(schema)
@@ -443,7 +456,7 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params
} }
buffer := WarningBuffer{} buffer := WarningBuffer{}
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, ns, &buffer)) k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, namespace, &buffer))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -2,26 +2,35 @@ package proxy
import ( import (
"net/http" "net/http"
"net/url"
"strings" "strings"
"testing" "testing"
"time" "time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/client"
"github.com/rancher/wrangler/v3/pkg/schemas"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake" "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
clientgotesting "k8s.io/client-go/testing" clientgotesting "k8s.io/client-go/testing"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/wrangler/v3/pkg/schemas"
"github.com/rancher/steve/pkg/client"
) )
var c *watch.FakeWatcher var c *watch.FakeWatcher
@@ -32,6 +41,14 @@ type testFactory struct {
fakeClient *fake.FakeDynamicClient fakeClient *fake.FakeDynamicClient
} }
func (t *testFactory) TableAdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
return t.fakeClient.Resource(schema2.GroupVersionResource{}), nil
}
func (t *testFactory) TableClient(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
return t.fakeClient.Resource(schema2.GroupVersionResource{}).Namespace(namespace), nil
}
func TestWatchNamesErrReceive(t *testing.T) { func TestWatchNamesErrReceive(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false) testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.Nil(t, err) assert.Nil(t, err)
@@ -80,10 +97,6 @@ func TestByNames(t *testing.T) {
assert.Nil(t, warn) assert.Nil(t, warn)
} }
func (t *testFactory) TableAdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
return t.fakeClient.Resource(schema2.GroupVersionResource{}), nil
}
func receiveUntil(wc chan watch.Event, d time.Duration) error { func receiveUntil(wc chan watch.Event, d time.Duration) error {
timer := time.NewTicker(d) timer := time.NewTicker(d)
defer timer.Stop() defer timer.Stop()
@@ -121,3 +134,357 @@ func receiveUntil(wc chan watch.Event, d time.Duration) error {
} }
} }
} }
func TestCreate(t *testing.T) {
type input struct {
apiOp *types.APIRequest
schema *types.APISchema
params types.APIObject
}
type expected struct {
value *unstructured.Unstructured
warning []types.Warning
err error
}
testCases := []struct {
name string
namespace string
input input
expected expected
createReactorFunc clientgotesting.ReactionFunc
}{
{
name: "creating resource - namespace scoped",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "creating resource - cluster scoped",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": false,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing name",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"namespace": "testing-ns",
"generateName": "testing-gen-name",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"generateName": "testing-gen-name",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing name / generateName",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"namespace": "testing-ns",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"generateName": "t-",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing namespace in the params / should copy from apiOp",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Namespace: "testing-ns",
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing namespace - namespace scoped",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"namespaced": true,
},
},
},
params: types.APIObject{},
},
expected: expected{
value: nil,
warning: nil,
err: apierror.NewAPIError(
validation.InvalidBodyContent,
errNamespaceRequired,
),
},
},
{
name: "error response",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewUnauthorized("sample reason")
},
expected: expected{
value: nil,
warning: []types.Warning{},
err: apierrors.NewUnauthorized("sample reason"),
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.Nil(t, err)
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
if tt.createReactorFunc != nil {
fakeClient.PrependReactor("create", "*", tt.createReactorFunc)
}
testStore := Store{
clientGetter: &testFactory{Factory: testClientFactory,
fakeClient: fakeClient,
},
}
value, warning, err := testStore.Create(tt.input.apiOp, tt.input.schema, tt.input.params)
assert.Equal(t, tt.expected.value, value)
assert.Equal(t, tt.expected.warning, warning)
assert.Equal(t, tt.expected.err, err)
})
}
}

View File

@@ -15,22 +15,6 @@ import (
"sync" "sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/informer"
"github.com/rancher/lasso/pkg/cache/sql/informer/factory"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/steve/pkg/resources/common"
"github.com/rancher/steve/pkg/resources/virtual"
virtualCommon "github.com/rancher/steve/pkg/resources/virtual/common"
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
"github.com/rancher/wrangler/v3/pkg/data"
"github.com/rancher/wrangler/v3/pkg/schemas"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"github.com/rancher/wrangler/v3/pkg/summary"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@@ -45,9 +29,30 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/informer"
"github.com/rancher/lasso/pkg/cache/sql/informer/factory"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/wrangler/v3/pkg/data"
"github.com/rancher/wrangler/v3/pkg/schemas"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
"github.com/rancher/wrangler/v3/pkg/summary"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/steve/pkg/resources/common"
"github.com/rancher/steve/pkg/resources/virtual"
virtualCommon "github.com/rancher/steve/pkg/resources/virtual/common"
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
) )
const watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS" const (
watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
errNamespaceRequired = "metadata.namespace or apiOp.namespace are required"
)
var ( var (
paramScheme = runtime.NewScheme() paramScheme = runtime.NewScheme()
@@ -506,20 +511,27 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params
} }
name := types.Name(input) name := types.Name(input)
ns := types.Namespace(input) namespace := types.Namespace(input)
if name == "" && input.String("metadata", "generateName") == "" { generateName := input.String("metadata", "generateName")
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
if name == "" && generateName == "" {
input.SetNested(schema.ID[0:1]+"-", "metadata", "generateName")
} }
if ns == "" && apiOp.Namespace != "" {
ns = apiOp.Namespace if attributes.Namespaced(schema) && namespace == "" {
input.SetNested(ns, "metadata", "namespace") if apiOp.Namespace == "" {
return nil, nil, apierror.NewAPIError(validation.InvalidBodyContent, errNamespaceRequired)
}
namespace = apiOp.Namespace
input.SetNested(namespace, "metadata", "namespace")
} }
gvk := attributes.GVK(schema) gvk := attributes.GVK(schema)
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind() input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
buffer := WarningBuffer{} buffer := WarningBuffer{}
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, ns, &buffer)) k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, namespace, &buffer))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -9,6 +9,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/rancher/lasso/pkg/cache/sql/informer" "github.com/rancher/lasso/pkg/cache/sql/informer"
"github.com/rancher/lasso/pkg/cache/sql/informer/factory" "github.com/rancher/lasso/pkg/cache/sql/informer/factory"
@@ -20,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types" "github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/client" "github.com/rancher/steve/pkg/client"
"github.com/rancher/wrangler/v3/pkg/schemas" "github.com/rancher/wrangler/v3/pkg/schemas"
@@ -49,6 +53,10 @@ type testFactory struct {
fakeClient *fake.FakeDynamicClient fakeClient *fake.FakeDynamicClient
} }
func (t *testFactory) TableClient(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
return t.fakeClient.Resource(schema2.GroupVersionResource{}).Namespace(namespace), nil
}
func TestNewProxyStore(t *testing.T) { func TestNewProxyStore(t *testing.T) {
type testCase struct { type testCase struct {
description string description string
@@ -742,3 +750,357 @@ func receiveUntil(wc chan watch.Event, d time.Duration) error {
} }
} }
} }
func TestCreate(t *testing.T) {
type input struct {
apiOp *types.APIRequest
schema *types.APISchema
params types.APIObject
}
type expected struct {
value *unstructured.Unstructured
warning []types.Warning
err error
}
testCases := []struct {
name string
namespace string
input input
expected expected
createReactorFunc clientgotesting.ReactionFunc
}{
{
name: "creating resource - namespace scoped",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "creating resource - cluster scoped",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": false,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing name",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"namespace": "testing-ns",
"generateName": "testing-gen-name",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"generateName": "testing-gen-name",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing name / generateName",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"namespace": "testing-ns",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"generateName": "t-",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing namespace in the params / should copy from apiOp",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Namespace: "testing-ns",
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return false, ret, nil
},
expected: expected{
value: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
}},
warning: []types.Warning{},
err: nil,
},
},
{
name: "missing namespace - namespace scoped",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"namespaced": true,
},
},
},
params: types.APIObject{},
},
expected: expected{
value: nil,
warning: nil,
err: apierror.NewAPIError(
validation.InvalidBodyContent,
errNamespaceRequired,
),
},
},
{
name: "error response",
input: input{
apiOp: &types.APIRequest{
Schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
},
},
Request: &http.Request{URL: &url.URL{}},
},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "testing",
Attributes: map[string]interface{}{
"version": "v1",
"kind": "Secret",
"namespaced": true,
},
},
},
params: types.APIObject{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "testing-secret",
"namespace": "testing-ns",
},
},
},
},
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewUnauthorized("sample reason")
},
expected: expected{
value: nil,
warning: []types.Warning{},
err: apierrors.NewUnauthorized("sample reason"),
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.Nil(t, err)
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
if tt.createReactorFunc != nil {
fakeClient.PrependReactor("create", "*", tt.createReactorFunc)
}
testStore := Store{
clientGetter: &testFactory{Factory: testClientFactory,
fakeClient: fakeClient,
},
}
value, warning, err := testStore.Create(tt.input.apiOp, tt.input.schema, tt.input.params)
assert.Equal(t, tt.expected.value, value)
assert.Equal(t, tt.expected.warning, warning)
assert.Equal(t, tt.expected.err, err)
})
}
}