mirror of
https://github.com/niusmallnan/steve.git
synced 2025-09-02 05:34:25 +00:00
Shuffle around code and use rancher/apiserver
This commit is contained in:
@@ -5,11 +5,11 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/auth"
|
||||
"github.com/rancher/steve/pkg/client"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/router"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io"
|
||||
apiextensionsv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1"
|
||||
|
@@ -3,13 +3,13 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/apiserver/pkg/server"
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/apiserver/pkg/urlbuilder"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/auth"
|
||||
k8sproxy "github.com/rancher/steve/pkg/proxy"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/server"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/schemaserver/urlbuilder"
|
||||
"github.com/rancher/steve/pkg/server/router"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
@@ -2,9 +2,9 @@ package handler
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
)
|
||||
|
||||
func k8sAPI(sf schema.Factory, apiOp *types.APIRequest) {
|
||||
|
@@ -1,69 +0,0 @@
|
||||
package apigroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
func Template(discovery discovery.DiscoveryInterface) schema.Template {
|
||||
return schema.Template{
|
||||
ID: "apigroup",
|
||||
Customize: func(apiSchema *types.APISchema) {
|
||||
apiSchema.CollectionMethods = []string{http.MethodGet}
|
||||
apiSchema.ResourceMethods = []string{http.MethodGet}
|
||||
},
|
||||
Formatter: func(request *types.APIRequest, resource *types.RawResource) {
|
||||
resource.ID = resource.APIObject.Data().String("name")
|
||||
},
|
||||
Store: NewStore(discovery),
|
||||
}
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
empty.Store
|
||||
|
||||
discovery discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
func NewStore(discovery discovery.DiscoveryInterface) types.Store {
|
||||
return &Store{
|
||||
Store: empty.Store{},
|
||||
discovery: discovery,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
return types.DefaultByID(e, apiOp, schema, id)
|
||||
}
|
||||
|
||||
func toAPIObject(schema *types.APISchema, group v1.APIGroup) types.APIObject {
|
||||
if group.Name == "" {
|
||||
group.Name = "core"
|
||||
}
|
||||
return types.APIObject{
|
||||
Type: schema.ID,
|
||||
ID: group.Name,
|
||||
Object: group,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
groupList, err := e.discovery.ServerGroups()
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
var result types.APIObjectList
|
||||
for _, item := range groupList.Groups {
|
||||
result.Objects = append(result.Objects, toAPIObject(schema, item))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
@@ -1,172 +0,0 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"github.com/rancher/steve/pkg/server/store/switchschema"
|
||||
"github.com/rancher/steve/pkg/server/store/switchstore"
|
||||
"github.com/rancher/wrangler/pkg/data"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
const (
|
||||
rancherCluster = "management.cattle.io.cluster"
|
||||
localID = "local"
|
||||
)
|
||||
|
||||
var (
|
||||
local = types.APIObject{
|
||||
Type: "cluster",
|
||||
ID: localID,
|
||||
Object: &Cluster{},
|
||||
}
|
||||
localList = types.APIObjectList{
|
||||
Objects: []types.APIObject{
|
||||
local,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func Register(ctx context.Context, schemas *types.APISchemas, cg proxy.ClientGetter, cluster clustercache.ClusterCache) error {
|
||||
k8s, err := cg.AdminK8sInterface()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shell := &shell{
|
||||
cg: cg,
|
||||
namespace: "dashboard-shells",
|
||||
}
|
||||
|
||||
picker := &picker{
|
||||
start: time.Now(),
|
||||
discovery: k8s.Discovery(),
|
||||
}
|
||||
|
||||
cluster.OnAdd(ctx, shell.PurgeOldShell)
|
||||
cluster.OnChange(ctx, func(gvr schema.GroupVersionResource, key string, obj, oldObj runtime.Object) error {
|
||||
return shell.PurgeOldShell(gvr, key, obj)
|
||||
})
|
||||
schemas.MustImportAndCustomize(Cluster{}, func(schema *types.APISchema) {
|
||||
schema.CollectionMethods = []string{http.MethodGet}
|
||||
schema.ResourceMethods = []string{http.MethodGet}
|
||||
schema.Formatter = Format
|
||||
schema.Store = &switchstore.Store{
|
||||
Picker: picker.Picker,
|
||||
}
|
||||
schema.LinkHandlers = map[string]http.Handler{
|
||||
"shell": shell,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Cluster struct {
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ClusterSpec `json:"spec"`
|
||||
Status ClusterStatus `json:"status"`
|
||||
}
|
||||
|
||||
type ClusterSpec struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
type ClusterStatus struct {
|
||||
Driver string `json:"driver"`
|
||||
Version *version.Info `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func Format(request *types.APIRequest, resource *types.RawResource) {
|
||||
copy := [][]string{
|
||||
{"spec", "displayName"},
|
||||
{"metadata", "creationTimestamp"},
|
||||
{"status", "driver"},
|
||||
{"status", "version"},
|
||||
}
|
||||
|
||||
from := resource.APIObject.Data()
|
||||
to := data.New()
|
||||
|
||||
for _, keys := range copy {
|
||||
to.SetNested(data.GetValueN(from, keys...), keys...)
|
||||
}
|
||||
|
||||
resource.APIObject.Object = to
|
||||
resource.Links["api"] = request.URLBuilder.RelativeToRoot("/k8s/clusters/" + resource.ID)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
empty.Store
|
||||
|
||||
start time.Time
|
||||
discovery discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
type picker struct {
|
||||
start time.Time
|
||||
discovery discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
func (p *picker) Picker(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (types.Store, error) {
|
||||
clusters := apiOp.Schemas.LookupSchema(rancherCluster)
|
||||
if clusters == nil {
|
||||
return &Store{
|
||||
start: p.start,
|
||||
discovery: p.discovery,
|
||||
}, nil
|
||||
}
|
||||
return &switchschema.Store{
|
||||
Schema: clusters,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
if id == localID {
|
||||
return s.newLocal(), nil
|
||||
}
|
||||
return types.APIObject{}, validation.NotFound
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
return types.APIObjectList{
|
||||
Objects: []types.APIObject{
|
||||
s.newLocal(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) newLocal() types.APIObject {
|
||||
cluster := &Cluster{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.NewTime(s.start),
|
||||
},
|
||||
Spec: ClusterSpec{
|
||||
DisplayName: "Remote",
|
||||
},
|
||||
Status: ClusterStatus{
|
||||
Driver: "remote",
|
||||
},
|
||||
}
|
||||
version, err := s.discovery.ServerVersion()
|
||||
if err == nil {
|
||||
cluster.Status.Version = version
|
||||
}
|
||||
return types.APIObject{
|
||||
Type: "cluster",
|
||||
ID: localID,
|
||||
Object: cluster,
|
||||
}
|
||||
}
|
@@ -1,404 +0,0 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"github.com/rancher/wrangler/pkg/condition"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
const (
|
||||
roleLabel = "shell.cattle.io/cluster-role"
|
||||
)
|
||||
|
||||
type shell struct {
|
||||
namespace string
|
||||
cg proxy.ClientGetter
|
||||
}
|
||||
|
||||
func (s *shell) PurgeOldShell(gvr schema.GroupVersionResource, key string, obj runtime.Object) error {
|
||||
if obj == nil ||
|
||||
gvr.Version != "v1" ||
|
||||
gvr.Group != rbacv1.GroupName ||
|
||||
gvr.Resource != "clusterroles" {
|
||||
return nil
|
||||
}
|
||||
|
||||
meta, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
// ignore error
|
||||
logrus.Warnf("failed to find metadata for %v, %s", gvr, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
if meta.GetLabels()[roleLabel] != "true" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if meta.GetCreationTimestamp().Add(time.Hour).Before(time.Now()) {
|
||||
client, err := s.cg.AdminK8sInterface()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
name := meta.GetName()
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
client.RbacV1().ClusterRoles().Delete(ctx, name, metav1.DeleteOptions{})
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *shell) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, user, client, err := s.contextAndClient(req)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := s.createRole(ctx, user, client)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
// Don't use request context as it already be canceled at this point
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
client.RbacV1().ClusterRoles().Delete(ctx, role.Name, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
pod, err := s.createPod(ctx, user, role, client)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s.proxyRequest(rw, req, pod, client)
|
||||
}
|
||||
|
||||
func (s *shell) proxyRequest(rw http.ResponseWriter, req *http.Request, pod *v1.Pod, client kubernetes.Interface) {
|
||||
attachURL := client.CoreV1().RESTClient().
|
||||
Get().
|
||||
Namespace(pod.Namespace).
|
||||
Resource("pods").
|
||||
Name(pod.Name).
|
||||
SubResource("attach").
|
||||
VersionedParams(&v1.PodAttachOptions{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
Stdin: false,
|
||||
Stdout: false,
|
||||
Stderr: false,
|
||||
TTY: false,
|
||||
Container: "shell",
|
||||
}, scheme.ParameterCodec).URL()
|
||||
|
||||
httpClient := client.CoreV1().RESTClient().(*rest.RESTClient).Client
|
||||
p := httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
req.URL = attachURL
|
||||
req.Host = attachURL.Host
|
||||
delete(req.Header, "Authorization")
|
||||
delete(req.Header, "Cookie")
|
||||
},
|
||||
Transport: httpClient.Transport,
|
||||
FlushInterval: time.Millisecond * 100,
|
||||
}
|
||||
|
||||
p.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (s *shell) contextAndClient(req *http.Request) (context.Context, user.Info, kubernetes.Interface, error) {
|
||||
ctx := req.Context()
|
||||
client, err := s.cg.AdminK8sInterface()
|
||||
if err != nil {
|
||||
return ctx, nil, nil, err
|
||||
}
|
||||
|
||||
user, ok := request.UserFrom(ctx)
|
||||
if !ok {
|
||||
return ctx, nil, nil, validation.Unauthorized
|
||||
}
|
||||
|
||||
return ctx, user, client, nil
|
||||
}
|
||||
|
||||
func (s *shell) createNamespace(ctx context.Context, client kubernetes.Interface) (*v1.Namespace, error) {
|
||||
ns, err := client.CoreV1().Namespaces().Get(ctx, s.namespace, metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.namespace,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
}
|
||||
return ns, err
|
||||
}
|
||||
|
||||
func (s *shell) createRole(ctx context.Context, user user.Info, client kubernetes.Interface) (*rbacv1.ClusterRole, error) {
|
||||
_, err := s.createNamespace(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RbacV1().ClusterRoles().Create(ctx, &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "dashboard-shell-",
|
||||
Labels: map[string]string{
|
||||
roleLabel: "true",
|
||||
},
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"impersonate"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"users"},
|
||||
ResourceNames: []string{user.GetName()},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"impersonate"},
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"groups"},
|
||||
ResourceNames: user.GetGroups(),
|
||||
},
|
||||
},
|
||||
AggregationRule: nil,
|
||||
}, metav1.CreateOptions{})
|
||||
|
||||
}
|
||||
|
||||
func (s *shell) createRoleBinding(ctx context.Context, role *rbacv1.ClusterRole, serviceAccount *v1.ServiceAccount, client kubernetes.Interface) error {
|
||||
_, err := client.RbacV1().ClusterRoleBindings().Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "dashboard-shell-",
|
||||
OwnerReferences: ref(role),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
APIGroup: "",
|
||||
Name: serviceAccount.Name,
|
||||
Namespace: serviceAccount.Namespace,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Kind: "ClusterRole",
|
||||
Name: role.Name,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func ref(role *rbacv1.ClusterRole) []metav1.OwnerReference {
|
||||
ref := metav1.OwnerReference{
|
||||
Name: role.Name,
|
||||
UID: role.UID,
|
||||
}
|
||||
ref.APIVersion, ref.Kind = rbacv1.SchemeGroupVersion.WithKind("ClusterRole").ToAPIVersionAndKind()
|
||||
return []metav1.OwnerReference{
|
||||
ref,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *shell) updateServiceAccount(ctx context.Context, pod *v1.Pod, serviceAccount *v1.ServiceAccount, client kubernetes.Interface) error {
|
||||
serviceAccount, err := client.CoreV1().ServiceAccounts(s.namespace).Get(ctx, serviceAccount.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceAccount.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
Name: pod.Name,
|
||||
UID: pod.UID,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = client.CoreV1().ServiceAccounts(s.namespace).Update(ctx, serviceAccount, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *shell) createServiceAccount(ctx context.Context, role *rbacv1.ClusterRole, client kubernetes.Interface) (*v1.ServiceAccount, error) {
|
||||
return client.CoreV1().ServiceAccounts(s.namespace).Create(ctx, &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "dashboard-shell-",
|
||||
OwnerReferences: ref(role),
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
func (s *shell) createConfigMap(ctx context.Context, user user.Info, role *rbacv1.ClusterRole, client kubernetes.Interface) (*v1.ConfigMap, error) {
|
||||
cfg := clientcmdapi.Config{
|
||||
Clusters: map[string]*clientcmdapi.Cluster{
|
||||
"cluster": {
|
||||
Server: "https://kubernetes.default",
|
||||
CertificateAuthority: "/run/secrets/kubernetes.io/serviceaccount/ca.crt",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{
|
||||
"user": {
|
||||
TokenFile: "/run/secrets/kubernetes.io/serviceaccount/token",
|
||||
Impersonate: user.GetName(),
|
||||
ImpersonateGroups: user.GetGroups(),
|
||||
},
|
||||
},
|
||||
Contexts: map[string]*clientcmdapi.Context{
|
||||
"default": {
|
||||
Cluster: "cluster",
|
||||
AuthInfo: "user",
|
||||
},
|
||||
},
|
||||
CurrentContext: "default",
|
||||
}
|
||||
|
||||
cfgData, err := clientcmd.Write(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.CoreV1().ConfigMaps(s.namespace).Create(ctx, &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "dashboard-shell-",
|
||||
Namespace: s.namespace,
|
||||
OwnerReferences: ref(role),
|
||||
},
|
||||
Data: map[string]string{
|
||||
"config": string(cfgData),
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
func (s *shell) createPod(ctx context.Context, user user.Info, role *rbacv1.ClusterRole, client kubernetes.Interface) (*v1.Pod, error) {
|
||||
sa, err := s.createServiceAccount(ctx, role, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.createRoleBinding(ctx, role, sa, client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cm, err := s.createConfigMap(ctx, user, role, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
zero := int64(0)
|
||||
t := true
|
||||
pod, err := client.CoreV1().Pods(s.namespace).Create(ctx, &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "dashboard-shell-",
|
||||
Namespace: s.namespace,
|
||||
OwnerReferences: ref(role),
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
TerminationGracePeriodSeconds: &zero,
|
||||
Volumes: []v1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: v1.VolumeSource{
|
||||
ConfigMap: &v1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: cm.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyNever,
|
||||
ServiceAccountName: sa.Name,
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "proxy",
|
||||
Image: "ibuildthecloud/shell:v0.0.1",
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
Env: []v1.EnvVar{
|
||||
{
|
||||
Name: "KUBECONFIG",
|
||||
Value: "/root/.kube/config",
|
||||
},
|
||||
},
|
||||
Command: []string{"kubectl", "proxy"},
|
||||
SecurityContext: &v1.SecurityContext{
|
||||
RunAsUser: &zero,
|
||||
RunAsGroup: &zero,
|
||||
ReadOnlyRootFilesystem: &t,
|
||||
},
|
||||
VolumeMounts: []v1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
ReadOnly: true,
|
||||
MountPath: "/root/.kube/config",
|
||||
SubPath: "config",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "shell",
|
||||
TTY: true,
|
||||
Stdin: true,
|
||||
StdinOnce: true,
|
||||
Image: "ibuildthecloud/shell:v0.0.1",
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
Command: []string{"bash"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ignore any error here
|
||||
err = s.updateServiceAccount(ctx, pod, sa, client)
|
||||
if err != nil {
|
||||
logrus.Warnf("failed to update service account %s/%s to be owned by pod %s/%s", sa.Namespace, sa.Name, pod.Namespace, pod.Name)
|
||||
}
|
||||
|
||||
sec := int64(60)
|
||||
resp, err := client.CoreV1().Pods(s.namespace).Watch(ctx, metav1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + pod.Name,
|
||||
ResourceVersion: pod.ResourceVersion,
|
||||
TimeoutSeconds: &sec,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Stop()
|
||||
|
||||
for event := range resp.ResultChan() {
|
||||
newPod, ok := event.Object.(*v1.Pod)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if condition.Cond(v1.PodReady).IsTrue(newPod) {
|
||||
return newPod, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to find shell")
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/ratelimit"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type DynamicColumns struct {
|
||||
client *rest.RESTClient
|
||||
}
|
||||
|
||||
type ColumnDefinition struct {
|
||||
metav1.TableColumnDefinition `json:",inline"`
|
||||
Field string `json:"field,omitempty"`
|
||||
}
|
||||
|
||||
func NewDynamicColumns(config *rest.Config) (*DynamicColumns, error) {
|
||||
c, err := newClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DynamicColumns{
|
||||
client: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DynamicColumns) SetColumns(ctx context.Context, schema *types.APISchema) error {
|
||||
if attributes.Columns(schema) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gvr := attributes.GVR(schema)
|
||||
if gvr.Resource == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := d.client.Get()
|
||||
if gvr.Group == "" {
|
||||
r.Prefix("api")
|
||||
} else {
|
||||
r.Prefix("apis", gvr.Group)
|
||||
}
|
||||
r.Prefix(gvr.Version)
|
||||
r.Prefix(gvr.Resource)
|
||||
r.VersionedParams(&metav1.ListOptions{
|
||||
Limit: 1,
|
||||
}, metav1.ParameterCodec)
|
||||
|
||||
obj, err := r.Do(ctx).Get()
|
||||
if err != nil {
|
||||
attributes.SetTable(schema, false)
|
||||
return nil
|
||||
}
|
||||
t, ok := obj.(*metav1.Table)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(t.ColumnDefinitions) > 0 {
|
||||
var cols []ColumnDefinition
|
||||
for i, colDef := range t.ColumnDefinitions {
|
||||
cols = append(cols, ColumnDefinition{
|
||||
TableColumnDefinition: colDef,
|
||||
Field: fmt.Sprintf("$.metadata.fields[%d]", i),
|
||||
})
|
||||
}
|
||||
attributes.SetColumns(schema, cols)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newClient(config *rest.Config) (*rest.RESTClient, error) {
|
||||
scheme := runtime.NewScheme()
|
||||
if err := internalversion.AddToScheme(scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := metav1.AddMetaToScheme(scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := metav1beta1.AddMetaToScheme(scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config = rest.CopyConfig(config)
|
||||
config.RateLimiter = ratelimit.None
|
||||
config.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
config.AcceptContentTypes = "application/json;as=Table;v=v1;g=meta.k8s.io"
|
||||
config.GroupVersion = &schema.GroupVersion{}
|
||||
config.NegotiatedSerializer = serializer.NewCodecFactory(scheme)
|
||||
config.APIPath = "/"
|
||||
return rest.RESTClientFor(config)
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"github.com/rancher/wrangler/pkg/data"
|
||||
"github.com/rancher/wrangler/pkg/summary"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func DefaultTemplate(clientGetter proxy.ClientGetter, asl accesscontrol.AccessSetLookup) schema.Template {
|
||||
return schema.Template{
|
||||
Store: proxy.NewProxyStore(clientGetter, asl),
|
||||
Formatter: Formatter,
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultFormatter(next types.Formatter) types.Formatter {
|
||||
return types.FormatterChain(Formatter, next)
|
||||
}
|
||||
|
||||
func Formatter(request *types.APIRequest, resource *types.RawResource) {
|
||||
meta, err := meta.Accessor(resource.APIObject.Object)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
selfLink := meta.GetSelfLink()
|
||||
if selfLink == "" {
|
||||
return
|
||||
}
|
||||
|
||||
u := request.URLBuilder.RelativeToRoot(selfLink)
|
||||
resource.Links["view"] = u
|
||||
|
||||
if _, ok := resource.Links["update"]; !ok {
|
||||
resource.Links["update"] = u
|
||||
}
|
||||
|
||||
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
|
||||
summary := summary.Summarize(unstr)
|
||||
data.PutValue(unstr.Object, map[string]interface{}{
|
||||
"name": summary.State,
|
||||
"error": summary.Error,
|
||||
"transitioning": summary.Transitioning,
|
||||
"message": strings.Join(summary.Message, ":"),
|
||||
}, "metadata", "state")
|
||||
}
|
||||
}
|
@@ -1,344 +0,0 @@
|
||||
package counts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/summary"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
ignore = map[string]bool{
|
||||
"count": true,
|
||||
"schema": true,
|
||||
"apiRoot": true,
|
||||
}
|
||||
)
|
||||
|
||||
func Register(schemas *types.APISchemas, ccache clustercache.ClusterCache) {
|
||||
schemas.MustImportAndCustomize(Count{}, func(schema *types.APISchema) {
|
||||
schema.CollectionMethods = []string{http.MethodGet}
|
||||
schema.ResourceMethods = []string{http.MethodGet}
|
||||
schema.Attributes["access"] = accesscontrol.AccessListByVerb{
|
||||
"watch": accesscontrol.AccessList{
|
||||
{
|
||||
Namespace: "*",
|
||||
ResourceName: "*",
|
||||
},
|
||||
},
|
||||
}
|
||||
schema.Store = &Store{
|
||||
ccache: ccache,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type Count struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Counts map[string]ItemCount `json:"counts"`
|
||||
}
|
||||
|
||||
type Summary struct {
|
||||
Count int `json:"count,omitempty"`
|
||||
States map[string]int `json:"states,omitempty"`
|
||||
Error int `json:"errors,omitempty"`
|
||||
Transitioning int `json:"transitioning,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Summary) DeepCopy() *Summary {
|
||||
r := *s
|
||||
if r.States != nil {
|
||||
r.States = map[string]int{}
|
||||
for k := range s.States {
|
||||
r.States[k] = s.States[k]
|
||||
}
|
||||
}
|
||||
return &r
|
||||
}
|
||||
|
||||
type ItemCount struct {
|
||||
Summary Summary `json:"summary,omitempty"`
|
||||
Namespaces map[string]Summary `json:"namespaces,omitempty"`
|
||||
Revision int `json:"revision,omitempty"`
|
||||
}
|
||||
|
||||
func (i *ItemCount) DeepCopy() *ItemCount {
|
||||
r := *i
|
||||
r.Summary = *r.Summary.DeepCopy()
|
||||
if r.Namespaces != nil {
|
||||
r.Namespaces = map[string]Summary{}
|
||||
for k, v := range i.Namespaces {
|
||||
r.Namespaces[k] = *v.DeepCopy()
|
||||
}
|
||||
}
|
||||
return &r
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
empty.Store
|
||||
ccache clustercache.ClusterCache
|
||||
}
|
||||
|
||||
func toAPIObject(c Count) types.APIObject {
|
||||
return types.APIObject{
|
||||
Type: "count",
|
||||
ID: c.ID,
|
||||
Object: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
c := s.getCount(apiOp)
|
||||
return toAPIObject(c), nil
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
c := s.getCount(apiOp)
|
||||
return types.APIObjectList{
|
||||
Objects: []types.APIObject{
|
||||
toAPIObject(c),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
var (
|
||||
result = make(chan types.APIEvent, 100)
|
||||
counts map[string]ItemCount
|
||||
gvrToSchema = map[schema2.GroupVersionResource]*types.APISchema{}
|
||||
countLock sync.Mutex
|
||||
)
|
||||
|
||||
counts = s.getCount(apiOp).Counts
|
||||
for id := range counts {
|
||||
schema := apiOp.Schemas.LookupSchema(id)
|
||||
if schema == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
gvrToSchema[attributes.GVR(schema)] = schema
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-apiOp.Context().Done()
|
||||
countLock.Lock()
|
||||
close(result)
|
||||
result = nil
|
||||
countLock.Unlock()
|
||||
}()
|
||||
|
||||
onChange := func(add bool, gvr schema2.GroupVersionResource, _ string, obj, oldObj runtime.Object) error {
|
||||
countLock.Lock()
|
||||
defer countLock.Unlock()
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
schema := gvrToSchema[gvr]
|
||||
if schema == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, namespace, revision, summary, ok := getInfo(obj)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
itemCount := counts[schema.ID]
|
||||
if revision <= itemCount.Revision {
|
||||
return nil
|
||||
}
|
||||
|
||||
if oldObj != nil {
|
||||
if _, _, _, oldSummary, ok := getInfo(oldObj); ok {
|
||||
if oldSummary.Transitioning == summary.Transitioning &&
|
||||
oldSummary.Error == summary.Error &&
|
||||
oldSummary.State == summary.State {
|
||||
return nil
|
||||
}
|
||||
itemCount = removeCounts(itemCount, namespace, oldSummary)
|
||||
itemCount = addCounts(itemCount, namespace, summary)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if add {
|
||||
itemCount = addCounts(itemCount, namespace, summary)
|
||||
} else {
|
||||
itemCount = removeCounts(itemCount, namespace, summary)
|
||||
}
|
||||
|
||||
counts[schema.ID] = itemCount
|
||||
countsCopy := map[string]ItemCount{}
|
||||
for k, v := range counts {
|
||||
countsCopy[k] = *v.DeepCopy()
|
||||
}
|
||||
|
||||
result <- types.APIEvent{
|
||||
Name: "resource.change",
|
||||
ResourceType: "counts",
|
||||
Object: toAPIObject(Count{
|
||||
ID: "count",
|
||||
Counts: countsCopy,
|
||||
}),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
s.ccache.OnAdd(apiOp.Context(), func(gvr schema2.GroupVersionResource, key string, obj runtime.Object) error {
|
||||
return onChange(true, gvr, key, obj, nil)
|
||||
})
|
||||
s.ccache.OnChange(apiOp.Context(), func(gvr schema2.GroupVersionResource, key string, obj, oldObj runtime.Object) error {
|
||||
return onChange(true, gvr, key, obj, oldObj)
|
||||
})
|
||||
s.ccache.OnRemove(apiOp.Context(), func(gvr schema2.GroupVersionResource, key string, obj runtime.Object) error {
|
||||
return onChange(false, gvr, key, obj, nil)
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) schemasToWatch(apiOp *types.APIRequest) (result []*types.APISchema) {
|
||||
for _, schema := range apiOp.Schemas.Schemas {
|
||||
if ignore[schema.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if schema.Store == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if apiOp.AccessControl.CanList(apiOp, schema) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if apiOp.AccessControl.CanWatch(apiOp, schema) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, schema)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getInfo(obj interface{}) (name string, namespace string, revision int, summaryResult summary.Summary, ok bool) {
|
||||
r, ok := obj.(runtime.Object)
|
||||
if !ok {
|
||||
return "", "", 0, summaryResult, false
|
||||
}
|
||||
|
||||
meta, err := meta.Accessor(r)
|
||||
if err != nil {
|
||||
return "", "", 0, summaryResult, false
|
||||
}
|
||||
|
||||
revision, err = strconv.Atoi(meta.GetResourceVersion())
|
||||
if err != nil {
|
||||
return "", "", 0, summaryResult, false
|
||||
}
|
||||
|
||||
summaryResult = summary.Summarize(r)
|
||||
return meta.GetName(), meta.GetNamespace(), revision, summaryResult, true
|
||||
}
|
||||
|
||||
func removeCounts(itemCount ItemCount, ns string, summary summary.Summary) ItemCount {
|
||||
itemCount.Summary = removeSummary(itemCount.Summary, summary)
|
||||
if ns != "" {
|
||||
itemCount.Namespaces[ns] = removeSummary(itemCount.Namespaces[ns], summary)
|
||||
}
|
||||
return itemCount
|
||||
}
|
||||
|
||||
func addCounts(itemCount ItemCount, ns string, summary summary.Summary) ItemCount {
|
||||
itemCount.Summary = addSummary(itemCount.Summary, summary)
|
||||
if ns != "" {
|
||||
itemCount.Namespaces[ns] = addSummary(itemCount.Namespaces[ns], summary)
|
||||
}
|
||||
return itemCount
|
||||
}
|
||||
|
||||
func removeSummary(counts Summary, summary summary.Summary) Summary {
|
||||
counts.Count--
|
||||
if summary.Transitioning {
|
||||
counts.Transitioning--
|
||||
}
|
||||
if summary.Error {
|
||||
counts.Error--
|
||||
}
|
||||
if summary.State != "" {
|
||||
if counts.States == nil {
|
||||
counts.States = map[string]int{}
|
||||
}
|
||||
counts.States[summary.State] -= 1
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func addSummary(counts Summary, summary summary.Summary) Summary {
|
||||
counts.Count++
|
||||
if summary.Transitioning {
|
||||
counts.Transitioning++
|
||||
}
|
||||
if summary.Error {
|
||||
counts.Error++
|
||||
}
|
||||
if summary.State != "" {
|
||||
if counts.States == nil {
|
||||
counts.States = map[string]int{}
|
||||
}
|
||||
counts.States[summary.State] += 1
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func (s *Store) getCount(apiOp *types.APIRequest) Count {
|
||||
counts := map[string]ItemCount{}
|
||||
|
||||
for _, schema := range s.schemasToWatch(apiOp) {
|
||||
gvr := attributes.GVR(schema)
|
||||
access, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||
|
||||
rev := 0
|
||||
itemCount := ItemCount{
|
||||
Namespaces: map[string]Summary{},
|
||||
}
|
||||
|
||||
all := access.Grants("list", "*", "*")
|
||||
|
||||
for _, obj := range s.ccache.List(gvr) {
|
||||
name, ns, revision, summary, ok := getInfo(obj)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !all && !access.Grants("list", ns, name) && !access.Grants("get", ns, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
if revision > rev {
|
||||
rev = revision
|
||||
}
|
||||
|
||||
itemCount = addCounts(itemCount, ns, summary)
|
||||
}
|
||||
|
||||
itemCount.Revision = rev
|
||||
counts[schema.ID] = itemCount
|
||||
}
|
||||
|
||||
return Count{
|
||||
ID: "count",
|
||||
Counts: counts,
|
||||
}
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schema/converter"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func DropHelmData(request *types.APIRequest, resource *types.RawResource) {
|
||||
data := resource.APIObject.Data()
|
||||
if data.String("metadata", "labels", "owner") == "helm" ||
|
||||
data.String("metadata", "labels", "OWNER") == "TILLER" {
|
||||
if data.String("data", "release") != "" {
|
||||
delete(data.Map("data"), "release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FormatRelease(request *types.APIRequest, resource *types.RawResource) {
|
||||
obj, ok := resource.APIObject.Object.(runtime.Object)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
release, err := ToRelease(obj, SchemeBasedNamespaceLookup(request.Schemas))
|
||||
if err == ErrNotHelmRelease {
|
||||
return
|
||||
} else if err != nil {
|
||||
logrus.Errorf("failed to render helm release: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
data = resource.APIObject.Data()
|
||||
namespace = data.String("metadata", "namespace")
|
||||
name = data.String("metadata", "name")
|
||||
)
|
||||
|
||||
switch data.String("kind") {
|
||||
case "Secret":
|
||||
resource.ID = namespace + "/s:" + name
|
||||
case "ConfigMap":
|
||||
resource.ID = namespace + "/c:" + name
|
||||
}
|
||||
|
||||
resource.Links["self"] = request.URLBuilder.ResourceLink(request.Schema, resource.ID)
|
||||
resource.APIObject.Object = release
|
||||
}
|
||||
|
||||
func SchemeBasedNamespaceLookup(schemas *types.APISchemas) IsNamespaced {
|
||||
return func(gvk schema.GroupVersionKind) bool {
|
||||
schema := schemas.LookupSchema(converter.GVKToSchemaID(gvk))
|
||||
return schema != nil && attributes.Namespaced(schema)
|
||||
}
|
||||
}
|
@@ -1,158 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes/timestamp"
|
||||
"github.com/sirupsen/logrus"
|
||||
rspb "k8s.io/helm/pkg/proto/hapi/release"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
readmes = map[string]bool{
|
||||
"readme": true,
|
||||
"readme.txt": true,
|
||||
"readme.md": true,
|
||||
}
|
||||
statusMapping = map[string]Status{
|
||||
"UNKNOWN": StatusUnknown,
|
||||
"DEPLOYED": StatusDeployed,
|
||||
"DELETED": StatusUninstalled,
|
||||
"SUPERSEDED": StatusSuperseded,
|
||||
"FAILED": StatusFailed,
|
||||
"DELETING": StatusUninstalling,
|
||||
"PENDING_INSTALL": StatusPendingInstall,
|
||||
"PENDING_UPGRADE": StatusPendingUpgrade,
|
||||
"PENDING_ROLLBACK": StatusPendingRollback,
|
||||
}
|
||||
)
|
||||
|
||||
func isHelm2(labels map[string]string) bool {
|
||||
return labels["OWNER"] == "TILLER"
|
||||
}
|
||||
|
||||
func fromHelm2Data(data string, isNamespaced IsNamespaced) (*Release, error) {
|
||||
release, err := decodeHelm2(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fromHelm2ReleaseToRelease(release, isNamespaced)
|
||||
}
|
||||
|
||||
func toTime(t *timestamp.Timestamp) time.Time {
|
||||
return time.Unix(t.GetSeconds(), int64(t.GetNanos())).UTC()
|
||||
}
|
||||
|
||||
func fromHelm2ReleaseToRelease(release *rspb.Release, isNamespaced IsNamespaced) (*Release, error) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
hr := &Release{
|
||||
Name: release.Name,
|
||||
Info: &Info{
|
||||
FirstDeployed: toTime(release.GetInfo().GetFirstDeployed()),
|
||||
LastDeployed: toTime(release.GetInfo().GetLastDeployed()),
|
||||
Deleted: toTime(release.GetInfo().GetDeleted()),
|
||||
Description: release.GetInfo().GetDescription(),
|
||||
Status: statusMapping[release.GetInfo().GetStatus().GetCode().String()],
|
||||
Notes: release.GetInfo().GetStatus().GetNotes(),
|
||||
},
|
||||
Chart: &Chart{
|
||||
Values: toMap(release.Namespace, release.Name, release.GetChart().GetValues().GetRaw()),
|
||||
Metadata: &Metadata{
|
||||
Name: release.GetChart().GetMetadata().GetName(),
|
||||
Home: release.GetChart().GetMetadata().GetHome(),
|
||||
Sources: release.GetChart().GetMetadata().GetSources(),
|
||||
Version: release.GetChart().GetMetadata().GetVersion(),
|
||||
Description: release.GetChart().GetMetadata().GetDescription(),
|
||||
Keywords: release.GetChart().GetMetadata().GetKeywords(),
|
||||
Icon: release.GetChart().GetMetadata().GetIcon(),
|
||||
Condition: release.GetChart().GetMetadata().GetCondition(),
|
||||
Tags: release.GetChart().GetMetadata().GetTags(),
|
||||
AppVersion: release.GetChart().GetMetadata().GetAppVersion(),
|
||||
Deprecated: release.GetChart().GetMetadata().GetDeprecated(),
|
||||
Annotations: release.GetChart().GetMetadata().GetAnnotations(),
|
||||
KubeVersion: release.GetChart().GetMetadata().GetKubeVersion(),
|
||||
},
|
||||
},
|
||||
Values: toMap(release.Namespace, release.Name, release.GetConfig().GetRaw()),
|
||||
Version: int(release.Version),
|
||||
Namespace: release.Namespace,
|
||||
HelmMajorVersion: 3,
|
||||
}
|
||||
|
||||
for _, m := range release.GetChart().GetMetadata().GetMaintainers() {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
hr.Chart.Metadata.Maintainers = append(hr.Chart.Metadata.Maintainers, Maintainer{
|
||||
Name: m.GetName(),
|
||||
Email: m.GetEmail(),
|
||||
URL: m.GetUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
for _, f := range release.GetChart().GetFiles() {
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
if readmes[strings.ToLower(f.TypeUrl)] {
|
||||
hr.Info.Readme = string(f.Value)
|
||||
}
|
||||
}
|
||||
|
||||
hr.Resources, err = resourcesFromManifest(release.Namespace, release.Manifest, isNamespaced)
|
||||
return hr, err
|
||||
}
|
||||
|
||||
func toMap(namespace, name string, manifest string) map[string]interface{} {
|
||||
values := map[string]interface{}{}
|
||||
|
||||
if manifest == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(manifest), &values); err != nil {
|
||||
logrus.Errorf("failed to unmarshal yaml for %s/%s", namespace, name)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func decodeHelm2(data string) (*rspb.Release, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For backwards compatibility with releases that were stored before
|
||||
// compression was introduced we skip decompression if the
|
||||
// gzip magic header is not found
|
||||
if bytes.Equal(b[0:3], magicGzip) {
|
||||
r, err := gzip.NewReader(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b2, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b = b2
|
||||
}
|
||||
|
||||
var rls rspb.Release
|
||||
// unmarshal protobuf bytes
|
||||
if err := proto.Unmarshal(b, &rls); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rls, nil
|
||||
}
|
@@ -1,132 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
)
|
||||
|
||||
func isHelm3(labels map[string]string) bool {
|
||||
return labels["owner"] == "helm"
|
||||
}
|
||||
|
||||
func fromHelm3Data(data string, isNamespaced IsNamespaced) (*Release, error) {
|
||||
release, err := decodeHelm3(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fromHelm3ReleaseToRelease(release, isNamespaced)
|
||||
}
|
||||
|
||||
func fromHelm3ReleaseToRelease(release *release.Release, isNamespaced IsNamespaced) (*Release, error) {
|
||||
var (
|
||||
info = &Info{}
|
||||
chart = &Chart{}
|
||||
err error
|
||||
)
|
||||
|
||||
if release.Info != nil {
|
||||
info = &Info{
|
||||
FirstDeployed: release.Info.FirstDeployed.Time,
|
||||
LastDeployed: release.Info.LastDeployed.Time,
|
||||
Deleted: release.Info.Deleted.Time,
|
||||
Description: release.Info.Description,
|
||||
Status: Status(release.Info.Status),
|
||||
Notes: release.Info.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
if release.Chart != nil {
|
||||
chart = &Chart{
|
||||
Values: release.Chart.Values,
|
||||
}
|
||||
if release.Chart.Metadata != nil {
|
||||
chart.Metadata = &Metadata{
|
||||
Name: release.Chart.Metadata.Name,
|
||||
Home: release.Chart.Metadata.Home,
|
||||
Sources: release.Chart.Metadata.Sources,
|
||||
Version: release.Chart.Metadata.Version,
|
||||
Description: release.Chart.Metadata.Description,
|
||||
Keywords: release.Chart.Metadata.Keywords,
|
||||
Icon: release.Chart.Metadata.Icon,
|
||||
APIVersion: release.Chart.Metadata.APIVersion,
|
||||
Condition: release.Chart.Metadata.Condition,
|
||||
Tags: release.Chart.Metadata.Tags,
|
||||
AppVersion: release.Chart.Metadata.AppVersion,
|
||||
Deprecated: release.Chart.Metadata.Deprecated,
|
||||
Annotations: release.Chart.Metadata.Annotations,
|
||||
KubeVersion: release.Chart.Metadata.KubeVersion,
|
||||
Type: release.Chart.Metadata.Type,
|
||||
}
|
||||
|
||||
for _, m := range release.Chart.Metadata.Maintainers {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
chart.Metadata.Maintainers = append(chart.Metadata.Maintainers, Maintainer{
|
||||
Name: m.Name,
|
||||
Email: m.Email,
|
||||
URL: m.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range release.Chart.Files {
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
if readmes[strings.ToLower(f.Name)] {
|
||||
info.Readme = string(f.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr := &Release{
|
||||
Name: release.Name,
|
||||
Info: info,
|
||||
Chart: chart,
|
||||
Values: release.Config,
|
||||
Resources: nil,
|
||||
Version: release.Version,
|
||||
Namespace: release.Namespace,
|
||||
HelmMajorVersion: 3,
|
||||
}
|
||||
|
||||
hr.Resources, err = resourcesFromManifest(release.Namespace, release.Manifest, isNamespaced)
|
||||
return hr, err
|
||||
}
|
||||
|
||||
func decodeHelm3(data string) (*release.Release, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For backwards compatibility with releases that were stored before
|
||||
// compression was introduced we skip decompression if the
|
||||
// gzip magic header is not found
|
||||
if bytes.Equal(b[0:3], magicGzip) {
|
||||
r, err := gzip.NewReader(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b2, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b = b2
|
||||
}
|
||||
|
||||
var rls release.Release
|
||||
// unmarshal release object bytes
|
||||
if err := json.Unmarshal(b, &rls); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rls, nil
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/partition"
|
||||
)
|
||||
|
||||
func Register(schemas *types.APISchemas) {
|
||||
schemas.InternalSchemas.TypeName("helmrelease", Release{})
|
||||
schemas.MustImportAndCustomize(Release{}, func(schema *types.APISchema) {
|
||||
schema.CollectionMethods = []string{http.MethodGet}
|
||||
schema.ResourceMethods = []string{http.MethodGet}
|
||||
schema.Store = &partition.Store{
|
||||
Partitioner: &partitioner{},
|
||||
}
|
||||
schema.Formatter = FormatRelease
|
||||
})
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/rancher/wrangler/pkg/data"
|
||||
"github.com/rancher/wrangler/pkg/yaml"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
meta2 "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotHelmRelease = errors.New("not helm release")
|
||||
magicGzip = []byte{0x1f, 0x8b, 0x08}
|
||||
)
|
||||
|
||||
type IsNamespaced func(gvk schema.GroupVersionKind) bool
|
||||
|
||||
func ToRelease(obj runtime.Object, isNamespaced IsNamespaced) (*Release, error) {
|
||||
releaseData, err := getReleaseDataAndKind(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isHelm3(meta.GetLabels()):
|
||||
return fromHelm3Data(releaseData, isNamespaced)
|
||||
case isHelm2(meta.GetLabels()):
|
||||
return fromHelm2Data(releaseData, isNamespaced)
|
||||
}
|
||||
|
||||
return nil, ErrNotHelmRelease
|
||||
}
|
||||
|
||||
func getReleaseDataAndKind(obj runtime.Object) (string, error) {
|
||||
switch t := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
releaseData := data.Object(t.Object).String("data", "release")
|
||||
switch t.GetKind() {
|
||||
case "ConfigMap":
|
||||
return releaseData, nil
|
||||
case "Secret":
|
||||
data, err := base64.StdEncoding.DecodeString(releaseData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
case *corev1.ConfigMap:
|
||||
return t.Data["release"], nil
|
||||
case *corev1.Secret:
|
||||
return string(t.Data["release"]), nil
|
||||
}
|
||||
|
||||
return "", ErrNotHelmRelease
|
||||
}
|
||||
|
||||
func resourcesFromManifest(namespace string, manifest string, isNamespaced IsNamespaced) (result []Resource, err error) {
|
||||
objs, err := yaml.ToObjects(bytes.NewReader([]byte(manifest)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range objs {
|
||||
meta, err := meta2.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := Resource{
|
||||
Name: meta.GetName(),
|
||||
Namespace: meta.GetNamespace(),
|
||||
}
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
if isNamespaced != nil && isNamespaced(gvk) && r.Namespace == "" {
|
||||
r.Namespace = namespace
|
||||
}
|
||||
r.APIVersion, r.Kind = gvk.ToAPIVersionAndKind()
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
@@ -1,108 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/partition"
|
||||
"github.com/rancher/steve/pkg/server/store/selector"
|
||||
"github.com/rancher/steve/pkg/server/store/switchschema"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
var (
|
||||
configMap2 = target{
|
||||
schemaType: "configmap",
|
||||
version: "2",
|
||||
selector: labels.SelectorFromSet(labels.Set{
|
||||
"OWNER": "TILLER",
|
||||
}),
|
||||
}
|
||||
secret2 = target{
|
||||
schemaType: "secret",
|
||||
version: "2",
|
||||
selector: labels.SelectorFromSet(labels.Set{
|
||||
"OWNER": "TILLER",
|
||||
}),
|
||||
}
|
||||
secret3 = target{
|
||||
schemaType: "secret",
|
||||
version: "3",
|
||||
selector: labels.SelectorFromSet(labels.Set{
|
||||
"owner": "helm",
|
||||
}),
|
||||
}
|
||||
all = []partition.Partition{
|
||||
configMap2,
|
||||
secret2,
|
||||
secret3,
|
||||
}
|
||||
)
|
||||
|
||||
type target struct {
|
||||
schemaType string
|
||||
version string
|
||||
selector labels.Selector
|
||||
}
|
||||
|
||||
func (t target) Name() string {
|
||||
return t.schemaType + t.version
|
||||
}
|
||||
|
||||
type partitioner struct {
|
||||
}
|
||||
|
||||
func (p *partitioner) Lookup(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (partition.Partition, error) {
|
||||
if id == "" {
|
||||
return nil, validation.Unauthorized
|
||||
}
|
||||
t := strings.SplitN(id, ":", 2)[0]
|
||||
if t == "c" {
|
||||
return configMap2, nil
|
||||
} else if t == "s" {
|
||||
return secret2, nil
|
||||
}
|
||||
return nil, validation.NotFound
|
||||
}
|
||||
|
||||
func (p *partitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, error) {
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (p *partitioner) Store(apiOp *types.APIRequest, partition partition.Partition) (types.Store, error) {
|
||||
target := partition.(target)
|
||||
schema := apiOp.Schemas.LookupSchema(target.schemaType)
|
||||
if schema == nil {
|
||||
return &empty.Store{}, nil
|
||||
}
|
||||
return &stripIDPrefix{
|
||||
Store: &selector.Store{
|
||||
Selector: target.selector,
|
||||
Store: &switchschema.Store{
|
||||
Schema: schema,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type stripIDPrefix struct {
|
||||
types.Store
|
||||
}
|
||||
|
||||
func stripPrefix(s string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(s, "c:"), "s:")
|
||||
}
|
||||
|
||||
func (s *stripIDPrefix) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
return s.Store.Delete(apiOp, schema, stripPrefix(id))
|
||||
}
|
||||
|
||||
func (s *stripIDPrefix) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
return s.Store.ByID(apiOp, schema, stripPrefix(id))
|
||||
}
|
||||
|
||||
func (s *stripIDPrefix) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
return s.Store.Update(apiOp, schema, data, stripPrefix(id))
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
// Name is the name of the release
|
||||
Name string `json:"name,omitempty"`
|
||||
// Info provides information about a release
|
||||
Info *Info `json:"info,omitempty"`
|
||||
// Chart is the chart that was released.
|
||||
Chart *Chart `json:"chart,omitempty"`
|
||||
// Config is the set of extra Values added to the chart.
|
||||
// These values override the default values inside of the chart.
|
||||
Values map[string]interface{} `json:"values,omitempty"`
|
||||
// Manifest is the string representation of the rendered template.
|
||||
Resources []Resource `json:"resources,omitempty"`
|
||||
// Version is an int which represents the version of the release.
|
||||
Version int `json:"version,omitempty"`
|
||||
// Namespace is the kubernetes namespace of the release.
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
HelmMajorVersion int `json:"helmVersion,omitempty"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// Chart is a helm package that contains metadata, a default config, zero or more
|
||||
// optionally parameterizable templates, and zero or more charts (dependencies).
|
||||
type Chart struct {
|
||||
// Metadata is the contents of the Chartfile.
|
||||
Metadata *Metadata `json:"metadata"`
|
||||
// Values are default config for this chart.
|
||||
Values map[string]interface{} `json:"values"`
|
||||
}
|
||||
|
||||
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
|
||||
type Metadata struct {
|
||||
// The name of the chart
|
||||
Name string `json:"name,omitempty"`
|
||||
// The URL to a relevant project page, git repo, or contact person
|
||||
Home string `json:"home,omitempty"`
|
||||
// Source is the URL to the source code of this chart
|
||||
Sources []string `json:"sources,omitempty"`
|
||||
// A SemVer 2 conformant version string of the chart
|
||||
Version string `json:"version,omitempty"`
|
||||
// A one-sentence description of the chart
|
||||
Description string `json:"description,omitempty"`
|
||||
// A list of string keywords
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
// A list of name and URL/email address combinations for the maintainer(s)
|
||||
Maintainers []Maintainer `json:"maintainers,omitempty"`
|
||||
// The URL to an icon file.
|
||||
Icon string `json:"icon,omitempty"`
|
||||
// The API Version of this chart.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
// The condition to check to enable chart
|
||||
Condition string `json:"condition,omitempty"`
|
||||
// The tags to check to enable chart
|
||||
Tags string `json:"tags,omitempty"`
|
||||
// The version of the application enclosed inside of this chart.
|
||||
AppVersion string `json:"appVersion,omitempty"`
|
||||
// Whether or not this chart is deprecated
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
// Annotations are additional mappings uninterpreted by Helm,
|
||||
// made available for inspection by other applications.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
|
||||
KubeVersion string `json:"kubeVersion,omitempty"`
|
||||
// Specifies the chart type: application or library
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Maintainer describes a Chart maintainer.
|
||||
type Maintainer struct {
|
||||
// Name is a user name or organization name
|
||||
Name string `json:"name,omitempty"`
|
||||
// Email is an optional email address to contact the named maintainer
|
||||
Email string `json:"email,omitempty"`
|
||||
// URL is an optional URL to an address for the named maintainer
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Info describes release information.
|
||||
type Info struct {
|
||||
// FirstDeployed is when the release was first deployed.
|
||||
FirstDeployed time.Time `json:"firstDeployed,omitempty"`
|
||||
// LastDeployed is when the release was last deployed.
|
||||
LastDeployed time.Time `json:"lastDeployed,omitempty"`
|
||||
// Deleted tracks when this object was deleted.
|
||||
Deleted time.Time `json:"deleted"`
|
||||
// Description is human-friendly "log entry" about this release.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Status is the current state of the release
|
||||
Status Status `json:"status,omitempty" wrangler:"options=unknown|deployed|uninstalled|superseded|failed|uninstalling|pending-install|pending-upgrade|pending-rollback"`
|
||||
// Contains the rendered templates/NOTES.txt if available
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
}
|
||||
|
||||
type Status string
|
||||
|
||||
// Describe the status of a release
|
||||
// NOTE: Make sure to update cmd/helm/status.go when adding or modifying any of these statuses.
|
||||
const (
|
||||
// StatusUnknown indicates that a release is in an uncertain state.
|
||||
StatusUnknown Status = "unknown"
|
||||
// StatusDeployed indicates that the release has been pushed to Kubernetes.
|
||||
StatusDeployed Status = "deployed"
|
||||
// StatusUninstalled indicates that a release has been uninstalled from Kubernetes.
|
||||
StatusUninstalled Status = "uninstalled"
|
||||
// StatusSuperseded indicates that this release object is outdated and a newer one exists.
|
||||
StatusSuperseded Status = "superseded"
|
||||
// StatusFailed indicates that the release was not successfully deployed.
|
||||
StatusFailed Status = "failed"
|
||||
// StatusUninstalling indicates that a uninstall operation is underway.
|
||||
StatusUninstalling Status = "uninstalling"
|
||||
// StatusPendingInstall indicates that an install operation is underway.
|
||||
StatusPendingInstall Status = "pending-install"
|
||||
// StatusPendingUpgrade indicates that an upgrade operation is underway.
|
||||
StatusPendingUpgrade Status = "pending-upgrade"
|
||||
// StatusPendingRollback indicates that an rollback operation is underway.
|
||||
StatusPendingRollback Status = "pending-rollback"
|
||||
)
|
@@ -1,47 +0,0 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/client"
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/apiroot"
|
||||
"github.com/rancher/steve/pkg/schemaserver/subscribe"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/resources/apigroups"
|
||||
"github.com/rancher/steve/pkg/server/resources/clusters"
|
||||
"github.com/rancher/steve/pkg/server/resources/common"
|
||||
"github.com/rancher/steve/pkg/server/resources/counts"
|
||||
"github.com/rancher/steve/pkg/server/resources/helm"
|
||||
"github.com/rancher/steve/pkg/server/resources/userpreferences"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
func DefaultSchemas(ctx context.Context, baseSchema *types.APISchemas, ccache clustercache.ClusterCache, cg proxy.ClientGetter) (*types.APISchemas, error) {
|
||||
counts.Register(baseSchema, ccache)
|
||||
subscribe.Register(baseSchema)
|
||||
apiroot.Register(baseSchema, []string{"v1"}, []string{"proxy:/apis"})
|
||||
userpreferences.Register(baseSchema, cg)
|
||||
helm.Register(baseSchema)
|
||||
|
||||
err := clusters.Register(ctx, baseSchema, cg, ccache)
|
||||
return baseSchema, err
|
||||
}
|
||||
|
||||
func DefaultSchemaTemplates(cf *client.Factory, lookup accesscontrol.AccessSetLookup, discovery discovery.DiscoveryInterface) []schema.Template {
|
||||
return []schema.Template{
|
||||
common.DefaultTemplate(cf, lookup),
|
||||
apigroups.Template(discovery),
|
||||
{
|
||||
ID: "configmap",
|
||||
Formatter: common.DefaultFormatter(helm.DropHelmData),
|
||||
},
|
||||
{
|
||||
ID: "secret",
|
||||
Formatter: common.DefaultFormatter(helm.DropHelmData),
|
||||
},
|
||||
}
|
||||
}
|
@@ -1,158 +0,0 @@
|
||||
package schemas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/builtin"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
schemastore "github.com/rancher/steve/pkg/schemaserver/store/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/broadcast"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func SetupWatcher(ctx context.Context, schemas *types.APISchemas, asl accesscontrol.AccessSetLookup, factory schema.Factory) {
|
||||
// one instance shared with all stores
|
||||
notifier := schemaChangeNotifier(ctx, factory)
|
||||
|
||||
schema := builtin.Schema
|
||||
schema.Store = &Store{
|
||||
Store: schema.Store,
|
||||
asl: asl,
|
||||
sf: factory,
|
||||
schemaChangeNotify: notifier,
|
||||
}
|
||||
|
||||
schemas.AddSchema(schema)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
types.Store
|
||||
|
||||
asl accesscontrol.AccessSetLookup
|
||||
sf schema.Factory
|
||||
schemaChangeNotify func(context.Context) (chan interface{}, error)
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
user, ok := request.UserFrom(apiOp.Request.Context())
|
||||
if !ok {
|
||||
return nil, validation.Unauthorized
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
result := make(chan types.APIEvent)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(result)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c, err := s.schemaChangeNotify(apiOp.Context())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
schemas, err := s.sf.Schemas(user)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to generate schemas for user %v: %v", user, err)
|
||||
return
|
||||
}
|
||||
for range c {
|
||||
schemas = s.sendSchemas(result, apiOp, user, schemas)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
schemas, err := s.sf.Schemas(user)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to generate schemas for notify user %v: %v", user, err)
|
||||
return
|
||||
}
|
||||
for range s.userChangeNotify(apiOp.Context(), user) {
|
||||
schemas = s.sendSchemas(result, apiOp, user, schemas)
|
||||
}
|
||||
}()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) sendSchemas(result chan types.APIEvent, apiOp *types.APIRequest, user user.Info, oldSchemas *types.APISchemas) *types.APISchemas {
|
||||
schemas, err := s.sf.Schemas(user)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to get schemas for %v: %v", user, err)
|
||||
return oldSchemas
|
||||
}
|
||||
|
||||
for _, apiObject := range schemastore.FilterSchemas(apiOp, schemas.Schemas).Objects {
|
||||
result <- types.APIEvent{
|
||||
Name: types.ChangeAPIEvent,
|
||||
ResourceType: "schema",
|
||||
Object: apiObject,
|
||||
}
|
||||
delete(oldSchemas.Schemas, apiObject.ID)
|
||||
}
|
||||
|
||||
for id, oldSchema := range oldSchemas.Schemas {
|
||||
result <- types.APIEvent{
|
||||
Name: types.ChangeAPIEvent,
|
||||
ResourceType: "schema",
|
||||
Object: types.APIObject{
|
||||
Type: "schema",
|
||||
ID: id,
|
||||
Object: oldSchema,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return schemas
|
||||
}
|
||||
|
||||
func (s *Store) userChangeNotify(ctx context.Context, user user.Info) chan interface{} {
|
||||
as := s.asl.AccessFor(user)
|
||||
result := make(chan interface{})
|
||||
go func() {
|
||||
defer close(result)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
newAS := s.asl.AccessFor(user)
|
||||
if newAS.ID != as.ID {
|
||||
result <- struct{}{}
|
||||
as = newAS
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func schemaChangeNotifier(ctx context.Context, factory schema.Factory) func(ctx context.Context) (chan interface{}, error) {
|
||||
notify := make(chan interface{})
|
||||
bcast := &broadcast.Broadcaster{}
|
||||
factory.OnChange(ctx, func() {
|
||||
select {
|
||||
case notify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
return func(ctx context.Context) (chan interface{}, error) {
|
||||
return bcast.Subscribe(ctx, func() (chan interface{}, error) {
|
||||
return notify, nil
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,111 +0,0 @@
|
||||
package userpreferences
|
||||
|
||||
import (
|
||||
"github.com/rancher/norman/types/convert"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
type configMapStore struct {
|
||||
empty.Store
|
||||
cg proxy.ClientGetter
|
||||
}
|
||||
|
||||
func (e *configMapStore) getClient(apiOp *types.APIRequest) (v1.ConfigMapInterface, error) {
|
||||
c, err := e.cg.AdminK8sInterface()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.CoreV1().ConfigMaps("kube-system"), nil
|
||||
}
|
||||
|
||||
func newPref(u user.Info) (types.APIObject, *UserPreference) {
|
||||
pref := &UserPreference{
|
||||
Data: map[string]string{},
|
||||
}
|
||||
return types.APIObject{
|
||||
Type: "userpreference",
|
||||
ID: u.GetName(),
|
||||
Object: pref,
|
||||
}, pref
|
||||
}
|
||||
|
||||
func (e *configMapStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
u := getUser(apiOp)
|
||||
result, pref := newPref(u)
|
||||
|
||||
client, err := e.getClient(apiOp)
|
||||
if err == validation.NotFound {
|
||||
return result, nil
|
||||
} else if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
obj, err := client.Get(apiOp.Context(), prefName(u), metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
pref.Data = obj.Data
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *configMapStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
obj, err := e.ByID(apiOp, schema, "")
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
return types.APIObjectList{
|
||||
Objects: []types.APIObject{
|
||||
obj,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *configMapStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
u := getUser(apiOp)
|
||||
client, err := e.getClient(apiOp)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
values := map[string]string{}
|
||||
for k, v := range data.Data().Map("data") {
|
||||
values[k] = convert.ToString(v)
|
||||
}
|
||||
|
||||
obj, err := client.Get(apiOp.Context(), prefName(u), metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
_, err = client.Create(apiOp.Context(), &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: prefName(u),
|
||||
},
|
||||
Data: values,
|
||||
}, metav1.CreateOptions{})
|
||||
} else if err == nil {
|
||||
obj.Data = values
|
||||
_, err = client.Update(apiOp.Context(), obj, metav1.UpdateOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return e.ByID(apiOp, schema, "")
|
||||
}
|
||||
|
||||
func (e *configMapStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
u := getUser(apiOp)
|
||||
client, err := e.getClient(apiOp)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return types.APIObject{}, client.Delete(apiOp.Context(), prefName(u), metav1.DeleteOptions{})
|
||||
}
|
@@ -1,164 +0,0 @@
|
||||
package userpreferences
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"github.com/rancher/wrangler/pkg/data/convert"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
var (
|
||||
rancherSchema = "management.cattle.io.preference"
|
||||
)
|
||||
|
||||
type rancherPrefStore struct {
|
||||
empty.Store
|
||||
cg proxy.ClientGetter
|
||||
}
|
||||
|
||||
func (e *rancherPrefStore) getClient(apiOp *types.APIRequest) (dynamic.ResourceInterface, error) {
|
||||
u := getUser(apiOp).GetName()
|
||||
cmSchema := apiOp.Schemas.LookupSchema(rancherSchema)
|
||||
if cmSchema == nil {
|
||||
return nil, validation.NotFound
|
||||
}
|
||||
|
||||
return e.cg.AdminClient(apiOp, cmSchema, u)
|
||||
}
|
||||
|
||||
func (e *rancherPrefStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
u := getUser(apiOp)
|
||||
client, err := e.getClient(apiOp)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
pref := &UserPreference{
|
||||
Data: map[string]string{},
|
||||
}
|
||||
result := types.APIObject{
|
||||
Type: "userpreference",
|
||||
ID: u.GetName(),
|
||||
Object: pref,
|
||||
}
|
||||
|
||||
objs, err := client.List(apiOp.Context(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for _, obj := range objs.Items {
|
||||
pref.Data[obj.GetName()] = convert.ToString(obj.Object["value"])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *rancherPrefStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
obj, err := e.ByID(apiOp, schema, "")
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
return types.APIObjectList{
|
||||
Objects: []types.APIObject{
|
||||
obj,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *rancherPrefStore) createNamespace(apiOp *types.APIRequest, ns string) error {
|
||||
client, err := e.cg.AdminClient(apiOp, apiOp.Schemas.LookupSchema("namespace"), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.Get(apiOp.Context(), ns, metav1.GetOptions{})
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
_, err = client.Create(apiOp.Context(), &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": ns,
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *rancherPrefStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
client, err := e.getClient(apiOp)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
gvk := attributes.GVK(apiOp.Schemas.LookupSchema(rancherSchema))
|
||||
|
||||
newValues := map[string]string{}
|
||||
for k, v := range data.Data().Map("data") {
|
||||
newValues[k] = convert.ToString(v)
|
||||
}
|
||||
|
||||
prefs, err := client.List(apiOp.Context(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
for _, pref := range prefs.Items {
|
||||
key := pref.GetName()
|
||||
newValue, ok := newValues[key]
|
||||
delete(newValues, key)
|
||||
if ok && newValue != pref.Object["value"] {
|
||||
pref.Object["value"] = newValue
|
||||
_, err := client.Update(apiOp.Context(), &pref, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
} else if !ok {
|
||||
err := client.Delete(apiOp.Context(), key, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nsExists := false
|
||||
for k, v := range newValues {
|
||||
if !nsExists {
|
||||
if err := e.createNamespace(apiOp, getUser(apiOp).GetName()); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
nsExists = true
|
||||
}
|
||||
|
||||
_, err = client.Create(apiOp.Context(), &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": gvk.GroupVersion().String(),
|
||||
"kind": gvk.Kind,
|
||||
"metadata": map[string]interface{}{
|
||||
"name": k,
|
||||
},
|
||||
"value": v,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return e.ByID(apiOp, schema, "")
|
||||
}
|
||||
|
||||
func (e *rancherPrefStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
client, err := e.getClient(apiOp)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return types.APIObject{}, client.DeleteCollection(apiOp.Context(), metav1.DeleteOptions{}, metav1.ListOptions{})
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
package userpreferences
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"github.com/rancher/wrangler/pkg/name"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
type UserPreference struct {
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
func Register(schemas *types.APISchemas, cg proxy.ClientGetter) {
|
||||
schemas.InternalSchemas.TypeName("userpreference", UserPreference{})
|
||||
schemas.MustImportAndCustomize(UserPreference{}, func(schema *types.APISchema) {
|
||||
schema.CollectionMethods = []string{http.MethodGet}
|
||||
schema.ResourceMethods = []string{http.MethodGet, http.MethodPut, http.MethodDelete}
|
||||
schema.Store = New(cg)
|
||||
})
|
||||
}
|
||||
|
||||
func New(cg proxy.ClientGetter) types.Store {
|
||||
return &Store{
|
||||
rancher: &rancherPrefStore{
|
||||
cg: cg,
|
||||
},
|
||||
configMapStore: &configMapStore{
|
||||
cg: cg,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
empty.Store
|
||||
rancher *rancherPrefStore
|
||||
configMapStore *configMapStore
|
||||
}
|
||||
|
||||
func isRancher(apiOp *types.APIRequest) bool {
|
||||
return apiOp.Schemas.LookupSchema(rancherSchema) != nil
|
||||
}
|
||||
|
||||
func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
if isRancher(apiOp) {
|
||||
return e.rancher.ByID(apiOp, schema, id)
|
||||
}
|
||||
return e.configMapStore.ByID(apiOp, schema, id)
|
||||
}
|
||||
|
||||
func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
if isRancher(apiOp) {
|
||||
return e.rancher.List(apiOp, schema)
|
||||
}
|
||||
return e.configMapStore.List(apiOp, schema)
|
||||
}
|
||||
|
||||
func (e *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
if isRancher(apiOp) {
|
||||
return e.rancher.Update(apiOp, schema, data, id)
|
||||
}
|
||||
return e.configMapStore.Update(apiOp, schema, data, id)
|
||||
}
|
||||
|
||||
func (e *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
if isRancher(apiOp) {
|
||||
return e.rancher.Delete(apiOp, schema, id)
|
||||
}
|
||||
return e.configMapStore.Delete(apiOp, schema, id)
|
||||
}
|
||||
|
||||
func prefName(u user.Info) string {
|
||||
return name.SafeConcatName("pref", u.GetName())
|
||||
}
|
||||
|
||||
func getUser(apiOp *types.APIRequest) user.Info {
|
||||
u, ok := request.UserFrom(apiOp.Context())
|
||||
if !ok {
|
||||
u = &user.DefaultInfo{
|
||||
Name: "dashboard-user",
|
||||
}
|
||||
}
|
||||
return u
|
||||
}
|
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rancher/steve/pkg/schemaserver/urlbuilder"
|
||||
"github.com/rancher/apiserver/pkg/urlbuilder"
|
||||
)
|
||||
|
||||
type RouterFunc func(h Handlers) http.Handler
|
||||
|
@@ -5,18 +5,18 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/apiserver/pkg/types"
|
||||
"github.com/rancher/dynamiclistener/server"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/client"
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
schemacontroller "github.com/rancher/steve/pkg/controllers/schema"
|
||||
"github.com/rancher/steve/pkg/dashboard"
|
||||
"github.com/rancher/steve/pkg/resources"
|
||||
"github.com/rancher/steve/pkg/resources/common"
|
||||
"github.com/rancher/steve/pkg/resources/schemas"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/handler"
|
||||
"github.com/rancher/steve/pkg/server/resources"
|
||||
"github.com/rancher/steve/pkg/server/resources/common"
|
||||
"github.com/rancher/steve/pkg/server/resources/schemas"
|
||||
)
|
||||
|
||||
var ErrConfigRequired = errors.New("rest config is required")
|
||||
|
@@ -1,208 +0,0 @@
|
||||
package partition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
type Partition interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type ParallelPartitionLister struct {
|
||||
Lister PartitionLister
|
||||
Concurrency int64
|
||||
Partitions []Partition
|
||||
state *listState
|
||||
revision string
|
||||
err error
|
||||
}
|
||||
|
||||
type PartitionLister func(ctx context.Context, partition Partition, cont string, revision string, limit int) (types.APIObjectList, error)
|
||||
|
||||
func (p *ParallelPartitionLister) Err() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
func (p *ParallelPartitionLister) Revision() string {
|
||||
return p.revision
|
||||
}
|
||||
|
||||
func (p *ParallelPartitionLister) Continue() string {
|
||||
if p.state == nil {
|
||||
return ""
|
||||
}
|
||||
bytes, err := json.Marshal(p.state)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func indexOrZero(partitions []Partition, name string) int {
|
||||
if name == "" {
|
||||
return 0
|
||||
}
|
||||
for i, partition := range partitions {
|
||||
if partition.Name() == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *ParallelPartitionLister) List(ctx context.Context, limit int, resume string) (<-chan []types.APIObject, error) {
|
||||
var state listState
|
||||
if resume != "" {
|
||||
bytes, err := base64.StdEncoding.DecodeString(resume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if state.Limit > 0 {
|
||||
limit = state.Limit
|
||||
}
|
||||
}
|
||||
|
||||
result := make(chan []types.APIObject)
|
||||
go p.feeder(ctx, state, limit, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type listState struct {
|
||||
Revision string `json:"r,omitempty"`
|
||||
PartitionName string `json:"p,omitempty"`
|
||||
Continue string `json:"c,omitempty"`
|
||||
Offset int `json:"o,omitempty"`
|
||||
Limit int `json:"l,omitempty"`
|
||||
}
|
||||
|
||||
func (p *ParallelPartitionLister) feeder(ctx context.Context, state listState, limit int, result chan []types.APIObject) {
|
||||
var (
|
||||
sem = semaphore.NewWeighted(p.Concurrency)
|
||||
capacity = limit
|
||||
last chan struct{}
|
||||
)
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
defer func() {
|
||||
err := eg.Wait()
|
||||
if p.err == nil {
|
||||
p.err = err
|
||||
}
|
||||
close(result)
|
||||
}()
|
||||
|
||||
for i := indexOrZero(p.Partitions, state.PartitionName); i < len(p.Partitions); i++ {
|
||||
if capacity <= 0 || isDone(ctx) {
|
||||
break
|
||||
}
|
||||
|
||||
var (
|
||||
partition = p.Partitions[i]
|
||||
tickets = int64(1)
|
||||
turn = last
|
||||
next = make(chan struct{})
|
||||
)
|
||||
|
||||
// setup a linked list of channel to control insertion order
|
||||
last = next
|
||||
|
||||
if state.Revision == "" {
|
||||
// don't have a revision yet so grab all tickets to set a revision
|
||||
tickets = 3
|
||||
}
|
||||
if err := sem.Acquire(ctx, tickets); err != nil {
|
||||
p.err = err
|
||||
break
|
||||
}
|
||||
|
||||
// make state local
|
||||
state := state
|
||||
eg.Go(func() error {
|
||||
defer sem.Release(tickets)
|
||||
defer close(next)
|
||||
|
||||
for {
|
||||
cont := ""
|
||||
if partition.Name() == state.PartitionName {
|
||||
cont = state.Continue
|
||||
}
|
||||
list, err := p.Lister(ctx, partition, cont, state.Revision, limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
waitForTurn(ctx, turn)
|
||||
if p.state != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if state.Revision == "" {
|
||||
state.Revision = list.Revision
|
||||
}
|
||||
|
||||
if p.revision == "" {
|
||||
p.revision = list.Revision
|
||||
}
|
||||
|
||||
if state.PartitionName == partition.Name() && state.Offset > 0 && state.Offset < len(list.Objects) {
|
||||
list.Objects = list.Objects[state.Offset:]
|
||||
}
|
||||
|
||||
if len(list.Objects) > capacity {
|
||||
result <- list.Objects[:capacity]
|
||||
// save state to redo this list at this offset
|
||||
p.state = &listState{
|
||||
Revision: list.Revision,
|
||||
PartitionName: partition.Name(),
|
||||
Continue: cont,
|
||||
Offset: capacity,
|
||||
Limit: limit,
|
||||
}
|
||||
capacity = 0
|
||||
return nil
|
||||
} else {
|
||||
result <- list.Objects
|
||||
capacity -= len(list.Objects)
|
||||
if list.Continue == "" {
|
||||
return nil
|
||||
}
|
||||
// loop again and get more data
|
||||
state.Continue = list.Continue
|
||||
state.PartitionName = partition.Name()
|
||||
state.Offset = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
p.err = eg.Wait()
|
||||
}
|
||||
|
||||
func waitForTurn(ctx context.Context, turn chan struct{}) {
|
||||
if turn == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-turn:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func isDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
@@ -1,177 +0,0 @@
|
||||
package partition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Partitioner interface {
|
||||
Lookup(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (Partition, error)
|
||||
All(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]Partition, error)
|
||||
Store(apiOp *types.APIRequest, partition Partition) (types.Store, error)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Partitioner Partitioner
|
||||
}
|
||||
|
||||
func (s *Store) getStore(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (types.Store, error) {
|
||||
p, err := s.Partitioner.Lookup(apiOp, schema, verb, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.Partitioner.Store(apiOp, p)
|
||||
}
|
||||
|
||||
func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
target, err := s.getStore(apiOp, schema, "delete", id)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return target.Delete(apiOp, schema, id)
|
||||
}
|
||||
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
target, err := s.getStore(apiOp, schema, "get", id)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return target.ByID(apiOp, schema, id)
|
||||
}
|
||||
|
||||
func (s *Store) listPartition(ctx context.Context, apiOp *types.APIRequest, schema *types.APISchema, partition Partition,
|
||||
cont string, revision string, limit int) (types.APIObjectList, error) {
|
||||
store, err := s.Partitioner.Store(apiOp, partition)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
req := apiOp.Clone()
|
||||
req.Request = req.Request.Clone(ctx)
|
||||
|
||||
values := req.Request.URL.Query()
|
||||
values.Set("continue", cont)
|
||||
values.Set("revision", revision)
|
||||
if limit > 0 {
|
||||
values.Set("limit", strconv.Itoa(limit))
|
||||
} else {
|
||||
values.Del("limit")
|
||||
}
|
||||
req.Request.URL.RawQuery = values.Encode()
|
||||
|
||||
return store.List(req, schema)
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
var (
|
||||
result types.APIObjectList
|
||||
)
|
||||
|
||||
paritions, err := s.Partitioner.All(apiOp, schema, "list")
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
lister := ParallelPartitionLister{
|
||||
Lister: func(ctx context.Context, partition Partition, cont string, revision string, limit int) (types.APIObjectList, error) {
|
||||
return s.listPartition(ctx, apiOp, schema, partition, cont, revision, limit)
|
||||
},
|
||||
Concurrency: 3,
|
||||
Partitions: paritions,
|
||||
}
|
||||
|
||||
resume := apiOp.Request.URL.Query().Get("continue")
|
||||
limit := getLimit(apiOp.Request)
|
||||
|
||||
list, err := lister.List(apiOp.Context(), limit, resume)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for items := range list {
|
||||
result.Objects = append(result.Objects, items...)
|
||||
}
|
||||
|
||||
result.Revision = lister.Revision()
|
||||
result.Continue = lister.Continue()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
|
||||
target, err := s.getStore(apiOp, schema, "create", "")
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return target.Create(apiOp, schema, data)
|
||||
}
|
||||
|
||||
func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
target, err := s.getStore(apiOp, schema, "update", id)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return target.Update(apiOp, schema, data, id)
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
partitions, err := s.Partitioner.All(apiOp, schema, "watch")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(apiOp.Context())
|
||||
apiOp = apiOp.WithContext(ctx)
|
||||
|
||||
eg := errgroup.Group{}
|
||||
response := make(chan types.APIEvent)
|
||||
|
||||
for _, partition := range partitions {
|
||||
store, err := s.Partitioner.Store(apiOp, partition)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
defer cancel()
|
||||
c, err := store.Watch(apiOp, schema, wr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range c {
|
||||
response <- i
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(response)
|
||||
<-ctx.Done()
|
||||
eg.Wait()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func getLimit(req *http.Request) int {
|
||||
limitString := req.URL.Query().Get("limit")
|
||||
limit, err := strconv.Atoi(limitString)
|
||||
if err != nil {
|
||||
limit = 0
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 100000
|
||||
}
|
||||
return limit
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/schemaserver/httperror"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
type errorStore struct {
|
||||
types.Store
|
||||
}
|
||||
|
||||
func (e *errorStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
data, err := e.Store.ByID(apiOp, schema, id)
|
||||
return data, translateError(err)
|
||||
}
|
||||
|
||||
func (e *errorStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
data, err := e.Store.List(apiOp, schema)
|
||||
return data, translateError(err)
|
||||
}
|
||||
|
||||
func (e *errorStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
|
||||
data, err := e.Store.Create(apiOp, schema, data)
|
||||
return data, translateError(err)
|
||||
|
||||
}
|
||||
|
||||
func (e *errorStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
data, err := e.Store.Update(apiOp, schema, data, id)
|
||||
return data, translateError(err)
|
||||
|
||||
}
|
||||
|
||||
func (e *errorStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
data, err := e.Store.Delete(apiOp, schema, id)
|
||||
return data, translateError(err)
|
||||
|
||||
}
|
||||
|
||||
func (e *errorStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
data, err := e.Store.Watch(apiOp, schema, wr)
|
||||
return data, translateError(err)
|
||||
}
|
||||
|
||||
func translateError(err error) error {
|
||||
if apiError, ok := err.(errors.APIStatus); ok {
|
||||
status := apiError.Status()
|
||||
return httperror.NewAPIError(validation.ErrorCode{
|
||||
Status: int(status.Code),
|
||||
Code: string(status.Reason),
|
||||
}, status.Message)
|
||||
}
|
||||
return err
|
||||
}
|
@@ -1,499 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/partition"
|
||||
"github.com/rancher/wrangler/pkg/data"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
var (
|
||||
lowerChars = regexp.MustCompile("[a-z]+")
|
||||
)
|
||||
|
||||
type ClientGetter interface {
|
||||
IsImpersonating() bool
|
||||
K8sInterface(ctx *types.APIRequest) (kubernetes.Interface, error)
|
||||
AdminK8sInterface() (kubernetes.Interface, error)
|
||||
Client(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
AdminClient(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
TableClient(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
TableAdminClient(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
TableClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
TableAdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
clientGetter ClientGetter
|
||||
}
|
||||
|
||||
func NewProxyStore(clientGetter ClientGetter, lookup accesscontrol.AccessSetLookup) types.Store {
|
||||
return &errorStore{
|
||||
Store: &WatchRefresh{
|
||||
Store: &partition.Store{
|
||||
Partitioner: &rbacPartitioner{
|
||||
proxyStore: &Store{
|
||||
clientGetter: clientGetter,
|
||||
},
|
||||
},
|
||||
},
|
||||
asl: lookup,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
result, err := s.byID(apiOp, schema, id)
|
||||
return toAPI(schema, result), err
|
||||
}
|
||||
|
||||
func decodeParams(apiOp *types.APIRequest, target runtime.Object) error {
|
||||
return metav1.ParameterCodec.DecodeParameters(apiOp.Request.URL.Query(), metav1.SchemeGroupVersion, target)
|
||||
}
|
||||
|
||||
func toAPI(schema *types.APISchema, obj runtime.Object) types.APIObject {
|
||||
if obj == nil || reflect.ValueOf(obj).IsNil() {
|
||||
return types.APIObject{}
|
||||
}
|
||||
|
||||
if unstr, ok := obj.(*unstructured.Unstructured); ok {
|
||||
obj = moveToUnderscore(unstr)
|
||||
}
|
||||
|
||||
apiObject := types.APIObject{
|
||||
Type: schema.ID,
|
||||
Object: obj,
|
||||
}
|
||||
|
||||
m, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return apiObject
|
||||
}
|
||||
|
||||
id := m.GetName()
|
||||
ns := m.GetNamespace()
|
||||
if ns != "" {
|
||||
id = fmt.Sprintf("%s/%s", ns, id)
|
||||
}
|
||||
|
||||
apiObject.ID = id
|
||||
return apiObject
|
||||
}
|
||||
|
||||
func (s *Store) byID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, error) {
|
||||
k8sClient, err := s.clientGetter.TableClient(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := metav1.GetOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, err := k8sClient.Get(apiOp.Context(), id, opts)
|
||||
rowToObject(obj)
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func moveFromUnderscore(obj map[string]interface{}) map[string]interface{} {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
for k := range types.ReservedFields {
|
||||
v, ok := obj["_"+k]
|
||||
delete(obj, "_"+k)
|
||||
delete(obj, k)
|
||||
if ok {
|
||||
obj[k] = v
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
func moveToUnderscore(obj *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for k := range types.ReservedFields {
|
||||
v, ok := obj.Object[k]
|
||||
if ok {
|
||||
delete(obj.Object, k)
|
||||
obj.Object["_"+k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func rowToObject(obj *unstructured.Unstructured) {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if obj.Object["kind"] != "Table" ||
|
||||
obj.Object["apiVersion"] != "meta.k8s.io/v1" {
|
||||
return
|
||||
}
|
||||
|
||||
items := tableToObjects(obj.Object)
|
||||
if len(items) == 1 {
|
||||
obj.Object = items[0].Object
|
||||
}
|
||||
}
|
||||
|
||||
func tableToList(obj *unstructured.UnstructuredList) {
|
||||
if obj.Object["kind"] != "Table" ||
|
||||
obj.Object["apiVersion"] != "meta.k8s.io/v1" {
|
||||
return
|
||||
}
|
||||
|
||||
obj.Items = tableToObjects(obj.Object)
|
||||
}
|
||||
|
||||
func tableToObjects(obj map[string]interface{}) []unstructured.Unstructured {
|
||||
var result []unstructured.Unstructured
|
||||
|
||||
rows, _ := obj["rows"].([]interface{})
|
||||
for _, row := range rows {
|
||||
m, ok := row.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cells := m["cells"]
|
||||
object, ok := m["object"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
data.PutValue(object, cells, "metadata", "fields")
|
||||
result = append(result, unstructured.Unstructured{
|
||||
Object: object,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Store) ByNames(apiOp *types.APIRequest, schema *types.APISchema, names sets.String) (types.APIObjectList, error) {
|
||||
adminClient, err := s.clientGetter.TableAdminClient(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
objs, err := s.list(apiOp, schema, adminClient)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
var filtered []types.APIObject
|
||||
for _, obj := range objs.Objects {
|
||||
if names.Has(obj.Name()) {
|
||||
filtered = append(filtered, obj)
|
||||
}
|
||||
}
|
||||
|
||||
objs.Objects = filtered
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
client, err := s.clientGetter.TableClient(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
return s.list(apiOp, schema, client)
|
||||
}
|
||||
|
||||
func (s *Store) list(apiOp *types.APIRequest, schema *types.APISchema, client dynamic.ResourceInterface) (types.APIObjectList, error) {
|
||||
opts := metav1.ListOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObjectList{}, nil
|
||||
}
|
||||
|
||||
resultList, err := client.List(apiOp.Context(), opts)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
tableToList(resultList)
|
||||
|
||||
result := types.APIObjectList{
|
||||
Revision: resultList.GetResourceVersion(),
|
||||
Continue: resultList.GetContinue(),
|
||||
}
|
||||
|
||||
for i := range resultList.Items {
|
||||
result.Objects = append(result.Objects, toAPI(schema, &resultList.Items[i]))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func returnErr(err error, c chan types.APIEvent) {
|
||||
c <- types.APIEvent{
|
||||
Name: "resource.error",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) listAndWatch(apiOp *types.APIRequest, k8sClient dynamic.ResourceInterface, schema *types.APISchema, w types.WatchRequest, result chan types.APIEvent) {
|
||||
rev := w.Revision
|
||||
if rev == "" {
|
||||
list, err := k8sClient.List(apiOp.Context(), metav1.ListOptions{
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
returnErr(errors.Wrapf(err, "failed to list %s", schema.ID), result)
|
||||
return
|
||||
}
|
||||
rev = list.GetResourceVersion()
|
||||
} else if rev == "-1" {
|
||||
rev = ""
|
||||
}
|
||||
|
||||
timeout := int64(60 * 30)
|
||||
watcher, err := k8sClient.Watch(apiOp.Context(), metav1.ListOptions{
|
||||
Watch: true,
|
||||
TimeoutSeconds: &timeout,
|
||||
ResourceVersion: rev,
|
||||
})
|
||||
if err != nil {
|
||||
returnErr(errors.Wrapf(err, "stopping watch for %s: %v", schema.ID, err), result)
|
||||
return
|
||||
}
|
||||
defer watcher.Stop()
|
||||
logrus.Debugf("opening watcher for %s", schema.ID)
|
||||
|
||||
go func() {
|
||||
<-apiOp.Request.Context().Done()
|
||||
watcher.Stop()
|
||||
}()
|
||||
|
||||
for event := range watcher.ResultChan() {
|
||||
if event.Type == watch.Error {
|
||||
continue
|
||||
}
|
||||
result <- s.toAPIEvent(apiOp, schema, event.Type, event.Object)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) WatchNames(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest, names sets.String) (chan types.APIEvent, error) {
|
||||
adminClient, err := s.clientGetter.TableAdminClientForWatch(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := s.watch(apiOp, schema, w, adminClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(chan types.APIEvent)
|
||||
go func() {
|
||||
defer close(result)
|
||||
for item := range c {
|
||||
if item.Error != nil && names.Has(item.Object.Name()) {
|
||||
result <- item
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
client, err := s.clientGetter.TableClientForWatch(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.watch(apiOp, schema, w, client)
|
||||
}
|
||||
|
||||
func (s *Store) watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest, client dynamic.ResourceInterface) (chan types.APIEvent, error) {
|
||||
result := make(chan types.APIEvent)
|
||||
go func() {
|
||||
s.listAndWatch(apiOp, client, schema, w, result)
|
||||
logrus.Debugf("closing watcher for %s", schema.ID)
|
||||
close(result)
|
||||
}()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) toAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, et watch.EventType, obj runtime.Object) types.APIEvent {
|
||||
name := types.ChangeAPIEvent
|
||||
switch et {
|
||||
case watch.Deleted:
|
||||
name = types.RemoveAPIEvent
|
||||
case watch.Added:
|
||||
name = types.CreateAPIEvent
|
||||
}
|
||||
|
||||
if unstr, ok := obj.(*unstructured.Unstructured); ok {
|
||||
rowToObject(unstr)
|
||||
}
|
||||
|
||||
event := types.APIEvent{
|
||||
Name: name,
|
||||
Object: toAPI(schema, obj),
|
||||
}
|
||||
|
||||
m, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return event
|
||||
}
|
||||
|
||||
event.Revision = m.GetResourceVersion()
|
||||
return event
|
||||
}
|
||||
|
||||
func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject) (types.APIObject, error) {
|
||||
var (
|
||||
resp *unstructured.Unstructured
|
||||
)
|
||||
|
||||
input := params.Data()
|
||||
|
||||
if input == nil {
|
||||
input = data.Object{}
|
||||
}
|
||||
|
||||
name := types.Name(input)
|
||||
ns := types.Namespace(input)
|
||||
if name == "" && input.String("metadata", "generateName") == "" {
|
||||
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
|
||||
}
|
||||
if ns == "" && apiOp.Namespace != "" {
|
||||
ns = apiOp.Namespace
|
||||
input.SetNested(ns, "metadata", "namespace")
|
||||
}
|
||||
|
||||
gvk := attributes.GVK(schema)
|
||||
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
||||
|
||||
k8sClient, err := s.clientGetter.TableClient(apiOp, schema, ns)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
opts := metav1.CreateOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
resp, err = k8sClient.Create(apiOp.Context(), &unstructured.Unstructured{Object: input}, opts)
|
||||
rowToObject(resp)
|
||||
return toAPI(schema, resp), err
|
||||
}
|
||||
|
||||
func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject, id string) (types.APIObject, error) {
|
||||
var (
|
||||
err error
|
||||
input = params.Data()
|
||||
)
|
||||
|
||||
ns := types.Namespace(input)
|
||||
k8sClient, err := s.clientGetter.TableClient(apiOp, schema, ns)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
if apiOp.Method == http.MethodPatch {
|
||||
bytes, err := ioutil.ReadAll(io.LimitReader(apiOp.Request.Body, 2<<20))
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
pType := apitypes.StrategicMergePatchType
|
||||
if apiOp.Request.Header.Get("content-type") == string(apitypes.JSONPatchType) {
|
||||
pType = apitypes.JSONPatchType
|
||||
}
|
||||
|
||||
opts := metav1.PatchOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
if pType == apitypes.StrategicMergePatchType {
|
||||
data := map[string]interface{}{}
|
||||
if err := json.Unmarshal(bytes, &data); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
data = moveFromUnderscore(data)
|
||||
bytes, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := k8sClient.Patch(apiOp.Context(), id, pType, bytes, opts)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return toAPI(schema, resp), nil
|
||||
}
|
||||
|
||||
resourceVersion := input.String("metadata", "resourceVersion")
|
||||
if resourceVersion == "" {
|
||||
return types.APIObject{}, fmt.Errorf("metadata.resourceVersion is required for update")
|
||||
}
|
||||
|
||||
opts := metav1.UpdateOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
resp, err := k8sClient.Update(apiOp.Context(), &unstructured.Unstructured{Object: moveFromUnderscore(input)}, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
rowToObject(resp)
|
||||
return toAPI(schema, resp), nil
|
||||
}
|
||||
|
||||
func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
opts := metav1.DeleteOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, nil
|
||||
}
|
||||
|
||||
k8sClient, err := s.clientGetter.TableClient(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
if err := k8sClient.Delete(apiOp.Context(), id, opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
obj, err := s.byID(apiOp, schema, id)
|
||||
if err != nil {
|
||||
// ignore lookup error
|
||||
return types.APIObject{}, validation.ErrorCode{
|
||||
Status: http.StatusNoContent,
|
||||
}
|
||||
}
|
||||
return toAPI(schema, obj), nil
|
||||
}
|
@@ -1,185 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/partition"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
var (
|
||||
passthroughPartitions = []partition.Partition{
|
||||
Partition{Passthrough: true},
|
||||
}
|
||||
)
|
||||
|
||||
type filterKey struct{}
|
||||
|
||||
func AddNamespaceConstraint(req *http.Request, names ...string) *http.Request {
|
||||
set := sets.NewString(names...)
|
||||
ctx := context.WithValue(req.Context(), filterKey{}, set)
|
||||
return req.WithContext(ctx)
|
||||
}
|
||||
|
||||
func getNamespaceConstraint(req *http.Request) (sets.String, bool) {
|
||||
set, ok := req.Context().Value(filterKey{}).(sets.String)
|
||||
return set, ok
|
||||
}
|
||||
|
||||
type Partition struct {
|
||||
Namespace string
|
||||
All bool
|
||||
Passthrough bool
|
||||
Names sets.String
|
||||
}
|
||||
|
||||
func (p Partition) Name() string {
|
||||
return p.Namespace
|
||||
}
|
||||
|
||||
type rbacPartitioner struct {
|
||||
proxyStore *Store
|
||||
}
|
||||
|
||||
func (p *rbacPartitioner) Lookup(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (partition.Partition, error) {
|
||||
switch verb {
|
||||
case "get":
|
||||
fallthrough
|
||||
case "update":
|
||||
fallthrough
|
||||
case "delete":
|
||||
return passthroughPartitions[0], nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid verb %s", verb)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *rbacPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, error) {
|
||||
switch verb {
|
||||
case "list":
|
||||
fallthrough
|
||||
case "watch":
|
||||
partitions, passthrough := isPassthrough(apiOp, schema, verb)
|
||||
if passthrough {
|
||||
return passthroughPartitions, nil
|
||||
}
|
||||
sort.Slice(partitions, func(i, j int) bool {
|
||||
return partitions[i].(Partition).Namespace < partitions[j].(Partition).Namespace
|
||||
})
|
||||
return partitions, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid verb %s", verb)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *rbacPartitioner) Store(apiOp *types.APIRequest, partition partition.Partition) (types.Store, error) {
|
||||
return &byNameOrNamespaceStore{
|
||||
Store: p.proxyStore,
|
||||
partition: partition.(Partition),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type byNameOrNamespaceStore struct {
|
||||
*Store
|
||||
partition Partition
|
||||
}
|
||||
|
||||
func (b *byNameOrNamespaceStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
if b.partition.Passthrough {
|
||||
return b.Store.List(apiOp, schema)
|
||||
}
|
||||
|
||||
apiOp.Namespace = b.partition.Namespace
|
||||
if b.partition.All {
|
||||
return b.Store.List(apiOp, schema)
|
||||
}
|
||||
return b.Store.ByNames(apiOp, schema, b.partition.Names)
|
||||
}
|
||||
|
||||
func (b *byNameOrNamespaceStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
if b.partition.Passthrough {
|
||||
return b.Store.Watch(apiOp, schema, wr)
|
||||
}
|
||||
|
||||
apiOp.Namespace = b.partition.Namespace
|
||||
if b.partition.All {
|
||||
return b.Store.Watch(apiOp, schema, wr)
|
||||
}
|
||||
return b.Store.WatchNames(apiOp, schema, wr, b.partition.Names)
|
||||
}
|
||||
|
||||
func isPassthrough(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
||||
partitions, passthrough := isPassthroughUnconstrained(apiOp, schema, verb)
|
||||
namespaces, ok := getNamespaceConstraint(apiOp.Request)
|
||||
if !ok {
|
||||
return partitions, passthrough
|
||||
}
|
||||
|
||||
var result []partition.Partition
|
||||
|
||||
if passthrough {
|
||||
for namespace := range namespaces {
|
||||
result = append(result, Partition{
|
||||
Namespace: namespace,
|
||||
All: true,
|
||||
})
|
||||
}
|
||||
return result, false
|
||||
}
|
||||
|
||||
for _, partition := range partitions {
|
||||
if namespaces.Has(partition.Name()) {
|
||||
result = append(result, partition)
|
||||
}
|
||||
}
|
||||
|
||||
return result, false
|
||||
}
|
||||
|
||||
func isPassthroughUnconstrained(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
||||
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||
if accessListByVerb.All(verb) {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
resources := accessListByVerb.Granted(verb)
|
||||
if apiOp.Namespace != "" {
|
||||
if resources[apiOp.Namespace].All {
|
||||
return nil, true
|
||||
} else {
|
||||
return []partition.Partition{
|
||||
Partition{
|
||||
Namespace: apiOp.Namespace,
|
||||
Names: resources[apiOp.Namespace].Names,
|
||||
},
|
||||
}, false
|
||||
}
|
||||
}
|
||||
|
||||
var result []partition.Partition
|
||||
|
||||
if attributes.Namespaced(schema) {
|
||||
for k, v := range resources {
|
||||
result = append(result, Partition{
|
||||
Namespace: k,
|
||||
All: v.All,
|
||||
Names: v.Names,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for _, v := range resources {
|
||||
result = append(result, Partition{
|
||||
All: v.All,
|
||||
Names: v.Names,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, false
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
type WatchRefresh struct {
|
||||
types.Store
|
||||
asl accesscontrol.AccessSetLookup
|
||||
}
|
||||
|
||||
func (w *WatchRefresh) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
user, ok := request.UserFrom(apiOp.Context())
|
||||
if !ok {
|
||||
return w.Store.Watch(apiOp, schema, wr)
|
||||
}
|
||||
|
||||
as := w.asl.AccessFor(user)
|
||||
ctx, cancel := context.WithCancel(apiOp.Context())
|
||||
apiOp = apiOp.WithContext(ctx)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(30 * time.Second):
|
||||
}
|
||||
|
||||
newAs := w.asl.AccessFor(user)
|
||||
if as.ID != newAs.ID {
|
||||
// RBAC changed
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w.Store.Watch(apiOp, schema, wr)
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
package selector
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
types.Store
|
||||
Selector labels.Selector
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
return s.Store.List(s.addSelector(apiOp), schema)
|
||||
}
|
||||
|
||||
func (s *Store) addSelector(apiOp *types.APIRequest) *types.APIRequest {
|
||||
|
||||
apiOp = apiOp.Clone()
|
||||
apiOp.Request = apiOp.Request.Clone(apiOp.Context())
|
||||
q := apiOp.Request.URL.Query()
|
||||
q.Add("labelSelector", s.Selector.String())
|
||||
apiOp.Request.URL.RawQuery = q.Encode()
|
||||
return apiOp
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
return s.Store.Watch(s.addSelector(apiOp), schema, w)
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package switchschema
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
Schema *types.APISchema
|
||||
}
|
||||
|
||||
func (e *Store) Delete(apiOp *types.APIRequest, oldSchema *types.APISchema, id string) (types.APIObject, error) {
|
||||
obj, err := e.Schema.Store.Delete(apiOp, e.Schema, id)
|
||||
obj.Type = oldSchema.ID
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (e *Store) ByID(apiOp *types.APIRequest, oldSchema *types.APISchema, id string) (types.APIObject, error) {
|
||||
obj, err := e.Schema.Store.ByID(apiOp, e.Schema, id)
|
||||
obj.Type = oldSchema.ID
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (e *Store) List(apiOp *types.APIRequest, oldSchema *types.APISchema) (types.APIObjectList, error) {
|
||||
obj, err := e.Schema.Store.List(apiOp, e.Schema)
|
||||
for i := range obj.Objects {
|
||||
obj.Objects[i].Type = oldSchema.ID
|
||||
}
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (e *Store) Create(apiOp *types.APIRequest, oldSchema *types.APISchema, data types.APIObject) (types.APIObject, error) {
|
||||
obj, err := e.Schema.Store.Create(apiOp, e.Schema, data)
|
||||
obj.Type = oldSchema.ID
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (e *Store) Update(apiOp *types.APIRequest, oldSchema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
obj, err := e.Schema.Store.Update(apiOp, e.Schema, data, id)
|
||||
obj.Type = oldSchema.ID
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (e *Store) Watch(apiOp *types.APIRequest, oldSchema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
c, err := e.Schema.Store.Watch(apiOp, e.Schema, wr)
|
||||
if err != nil || c == nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
result := make(chan types.APIEvent)
|
||||
go func() {
|
||||
defer close(result)
|
||||
for obj := range c {
|
||||
if obj.Object.Type == e.Schema.ID {
|
||||
obj.Object.Type = oldSchema.ID
|
||||
}
|
||||
result <- obj
|
||||
}
|
||||
}()
|
||||
|
||||
return result, nil
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
package switchstore
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
)
|
||||
|
||||
type StorePicker func(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (types.Store, error)
|
||||
|
||||
type Store struct {
|
||||
Picker StorePicker
|
||||
}
|
||||
|
||||
func (e *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
s, err := e.Picker(apiOp, schema, "delete", id)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
return s.Delete(apiOp, schema, id)
|
||||
}
|
||||
|
||||
func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
s, err := e.Picker(apiOp, schema, "get", id)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
return s.ByID(apiOp, schema, id)
|
||||
}
|
||||
|
||||
func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
s, err := e.Picker(apiOp, schema, "list", "")
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
return s.List(apiOp, schema)
|
||||
}
|
||||
|
||||
func (e *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
|
||||
s, err := e.Picker(apiOp, schema, "create", "")
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
return s.Create(apiOp, schema, data)
|
||||
}
|
||||
|
||||
func (e *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
s, err := e.Picker(apiOp, schema, "update", id)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
return s.Update(apiOp, schema, data, id)
|
||||
}
|
||||
|
||||
func (e *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
s, err := e.Picker(apiOp, schema, "watch", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Watch(apiOp, schema, wr)
|
||||
}
|
Reference in New Issue
Block a user