diff --git a/pkg/resources/cluster/apply.go b/pkg/resources/cluster/apply.go new file mode 100644 index 0000000..c36eb98 --- /dev/null +++ b/pkg/resources/cluster/apply.go @@ -0,0 +1,124 @@ +package cluster + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/pborman/uuid" + "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/steve/pkg/attributes" + steveschema "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/stores/proxy" + "github.com/rancher/wrangler/pkg/apply" + "github.com/rancher/wrangler/pkg/yaml" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +type Apply struct { + cg proxy.ClientGetter + schemaFactory steveschema.Factory +} + +func (a *Apply) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + var ( + apiContext = types.GetAPIContext(req.Context()) + input ApplyInput + ) + + if err := json.NewDecoder(req.Body).Decode(&input); err != nil { + apiContext.WriteError(err) + return + } + + objs, err := yaml.ToObjects(bytes.NewBufferString(input.YAML)) + if err != nil { + apiContext.WriteError(err) + return + } + + apply, err := a.createApply(apiContext) + if err != nil { + apiContext.WriteError(err) + return + } + + if err := apply.WithDefaultNamespace(input.DefaultNamespace).ApplyObjects(objs...); err != nil { + apiContext.WriteError(err) + return + } + + var result types.APIObjectList + + for _, obj := range objs { + result.Objects = append(result.Objects, a.toAPIObject(apiContext, obj, input.DefaultNamespace)) + } + + apiContext.WriteResponseList(http.StatusOK, result) +} + +func (a *Apply) toAPIObject(apiContext *types.APIRequest, obj runtime.Object, defaultNamespace string) types.APIObject { + if defaultNamespace == "" { + defaultNamespace = "default" + } + + result := types.APIObject{ + Object: obj, + } + + m, err := meta.Accessor(obj) + if err != nil { + return result + } + + schemaID := a.schemaFactory.ByGVK(obj.GetObjectKind().GroupVersionKind()) + apiSchema := apiContext.Schemas.LookupSchema(schemaID) + if apiSchema != nil { + id := m.GetName() + ns := m.GetNamespace() + + if ns == "" && attributes.Namespaced(apiSchema) { + ns = defaultNamespace + } + + if ns != "" { + id = fmt.Sprintf("%s/%s", ns, id) + } + result.ID = id + result.Type = apiSchema.ID + + if apiSchema.Store != nil { + apiContext := apiContext.Clone() + apiContext.Namespace = ns + if obj, err := apiSchema.Store.ByID(apiContext.Clone(), apiSchema, m.GetName()); err == nil { + return obj + } + } + } + + return result +} + +func (a *Apply) createApply(apiContext *types.APIRequest) (apply.Apply, error) { + client, err := a.cg.K8sInterface(apiContext) + if err != nil { + return nil, err + } + + apply := apply.New(client.Discovery(), func(gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error) { + dynamicClient, err := a.cg.DynamicClient(apiContext) + if err != nil { + return nil, err + } + return dynamicClient.Resource(gvr), nil + }) + + return apply. + WithDynamicLookup(). + WithContext(apiContext.Context()). + WithSetID(uuid.New()), nil +} diff --git a/pkg/resources/cluster/cluster.go b/pkg/resources/cluster/cluster.go new file mode 100644 index 0000000..70a5a96 --- /dev/null +++ b/pkg/resources/cluster/cluster.go @@ -0,0 +1,188 @@ +package cluster + +import ( + "context" + "net/http" + + "github.com/rancher/apiserver/pkg/store/empty" + "github.com/rancher/apiserver/pkg/types" + detector "github.com/rancher/kubernetes-provider-detector" + "github.com/rancher/steve/pkg/accesscontrol" + "github.com/rancher/steve/pkg/attributes" + steveschema "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/stores/proxy" + "github.com/rancher/wrangler/pkg/genericcondition" + "github.com/rancher/wrangler/pkg/schemas" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + schema2 "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" +) + +func Register(ctx context.Context, apiSchemas *types.APISchemas, cg proxy.ClientGetter, schemaFactory steveschema.Factory) { + apiSchemas.InternalSchemas.TypeName("management.cattle.io.cluster", Cluster{}) + apiSchemas.MustImportAndCustomize(Cluster{}, 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{ + provider: provider(ctx, cg), + discovery: discoveryClient(cg), + } + attributes.SetGVK(schema, schema2.GroupVersionKind{ + Group: "management.cattle.io", + Version: "v3", + Kind: "Cluster", + }) + + schema.ActionHandlers = map[string]http.Handler{ + "apply": &Apply{ + cg: cg, + schemaFactory: schemaFactory, + }, + } + schema.ResourceActions = map[string]schemas.Action{ + "apply": { + Input: "applyInput", + Output: "applyOutput", + }, + } + }) +} + +func discoveryClient(cg proxy.ClientGetter) discovery.DiscoveryInterface { + k8s, err := cg.AdminK8sInterface() + if err != nil { + return nil + } + return k8s.Discovery() +} + +func provider(ctx context.Context, cg proxy.ClientGetter) string { + var ( + provider string + err error + ) + + k8s, err := cg.AdminK8sInterface() + if err == nil { + provider, _ = detector.DetectProvider(ctx, k8s) + } + + return provider +} + +func AddApply(apiSchemas *types.APISchemas, schema *types.APISchema) { + if _, ok := schema.ActionHandlers["apply"]; ok { + return + } + cluster := apiSchemas.LookupSchema("management.cattle.io.cluster") + if cluster == nil { + return + } + + actionHandler, ok := cluster.ActionHandlers["apply"] + if !ok { + return + } + + if schema.ActionHandlers == nil { + schema.ActionHandlers = map[string]http.Handler{} + } + schema.ActionHandlers["apply"] = actionHandler + + if schema.ResourceActions == nil { + schema.ResourceActions = map[string]schemas.Action{} + } + schema.ResourceActions["apply"] = schemas.Action{ + Input: "applyInput", + Output: "applyOutput", + } +} + +type Store struct { + empty.Store + provider string + discovery discovery.DiscoveryInterface +} + +func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + if apiOp.Namespace == "" && id == "local" { + return s.getLocal(), nil + } + return s.Store.ByID(apiOp, schema, id) +} + +func (s *Store) getLocal() types.APIObject { + var ( + info *version.Info + ) + + if s.discovery != nil { + info, _ = s.discovery.ServerVersion() + } + return types.APIObject{ + ID: "local", + Object: &Cluster{ + TypeMeta: metav1.TypeMeta{ + Kind: "Cluster", + APIVersion: "management.cattle.io/v3", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "local", + }, + Spec: Spec{ + DisplayName: "Local Cluster", + Internal: true, + }, + Status: Status{ + Version: info, + Driver: "local", + Provider: s.provider, + Conditions: []genericcondition.GenericCondition{ + { + Type: "Ready", + Status: "True", + }, + }, + }, + }, + } + +} + +func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + if apiOp.Namespace != "" { + return s.Store.List(apiOp, schema) + } + + return types.APIObjectList{ + Objects: []types.APIObject{ + s.getLocal(), + }, + }, nil +} + +func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) { + result := make(chan types.APIEvent, 1) + go func() { + <-apiOp.Context().Done() + close(result) + }() + + result <- types.APIEvent{ + Name: "local", + ResourceType: "management.cattle.io.clusters", + ID: "local", + Object: s.getLocal(), + } + + return result, nil +} diff --git a/pkg/resources/cluster/cluster_type.go b/pkg/resources/cluster/cluster_type.go new file mode 100644 index 0000000..e20387d --- /dev/null +++ b/pkg/resources/cluster/cluster_type.go @@ -0,0 +1,27 @@ +package cluster + +import ( + "github.com/rancher/wrangler/pkg/genericcondition" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/version" +) + +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec Spec `json:"spec"` + Status Status `json:"status"` +} + +type Spec struct { + DisplayName string `json:"displayName" norman:"required"` + Internal bool `json:"internal,omitempty"` + Description string `json:"description"` +} + +type Status struct { + Conditions []genericcondition.GenericCondition `json:"conditions,omitempty"` + Driver string `json:"driver,omitempty"` + Provider string `json:"provider"` + Version *version.Info `json:"version,omitempty"` +} diff --git a/pkg/resources/cluster/rest.go b/pkg/resources/cluster/rest.go new file mode 100644 index 0000000..ec46e64 --- /dev/null +++ b/pkg/resources/cluster/rest.go @@ -0,0 +1,14 @@ +package cluster + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +type ApplyInput struct { + DefaultNamespace string `json:"defaultNamespace,omitempty"` + YAML string `json:"yaml,omitempty"` +} + +type ApplyOutput struct { + Resources []runtime.Object `json:"resources,omitempty"` +} diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go index 603ef88..ce041ed 100644 --- a/pkg/resources/schema.go +++ b/pkg/resources/schema.go @@ -10,23 +10,28 @@ import ( "github.com/rancher/steve/pkg/client" "github.com/rancher/steve/pkg/clustercache" "github.com/rancher/steve/pkg/resources/apigroups" + "github.com/rancher/steve/pkg/resources/cluster" "github.com/rancher/steve/pkg/resources/common" "github.com/rancher/steve/pkg/resources/counts" "github.com/rancher/steve/pkg/resources/formatters" "github.com/rancher/steve/pkg/schema" + steveschema "github.com/rancher/steve/pkg/schema" "github.com/rancher/steve/pkg/stores/proxy" "github.com/rancher/steve/pkg/summarycache" "k8s.io/client-go/discovery" ) -func DefaultSchemas(ctx context.Context, baseSchema *types.APISchemas, ccache clustercache.ClusterCache, cg proxy.ClientGetter) (*types.APISchemas, error) { +func DefaultSchemas(ctx context.Context, baseSchema *types.APISchemas, ccache clustercache.ClusterCache, + cg proxy.ClientGetter, schemaFactory steveschema.Factory) error { counts.Register(baseSchema, ccache) subscribe.Register(baseSchema) apiroot.Register(baseSchema, []string{"v1"}, "proxy:/apis") - return baseSchema, nil + cluster.Register(ctx, baseSchema, cg, schemaFactory) + return nil } func DefaultSchemaTemplates(cf *client.Factory, + baseSchemas *types.APISchemas, summaryCache *summarycache.SummaryCache, lookup accesscontrol.AccessSetLookup, discovery discovery.DiscoveryInterface) []schema.Template { @@ -45,5 +50,11 @@ func DefaultSchemaTemplates(cf *client.Factory, ID: "pod", Formatter: formatters.Pod, }, + { + ID: "management.cattle.io.cluster", + Customize: func(apiSchema *types.APISchema) { + cluster.AddApply(baseSchemas, apiSchema) + }, + }, } } diff --git a/pkg/server/server.go b/pkg/server/server.go index c209156..5e2191c 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -122,17 +122,16 @@ func setup(ctx context.Context, server *Server) error { ccache := clustercache.NewClusterCache(ctx, cf.AdminDynamicClient()) server.ClusterCache = ccache + sf := schema.NewCollection(ctx, server.BaseSchemas, asl) - server.BaseSchemas, err = resources.DefaultSchemas(ctx, server.BaseSchemas, ccache, cf) - if err != nil { + if err = resources.DefaultSchemas(ctx, server.BaseSchemas, ccache, server.ClientFactory, sf); err != nil { return err } - sf := schema.NewCollection(ctx, server.BaseSchemas, asl) summaryCache := summarycache.New(sf, ccache) summaryCache.Start(ctx) - for _, template := range resources.DefaultSchemaTemplates(cf, summaryCache, asl, server.controllers.K8s.Discovery()) { + for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery()) { sf.AddTemplate(template) }