Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

Add sub apply cmd #253

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions internal/cli/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cli

import (
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"

"github.com/substratusai/substratus/internal/cli/utils"
"github.com/substratusai/substratus/internal/tui"
)

func applyCommand() *cobra.Command {
var flags struct {
namespace string
filename string
kubeconfig string
}

run := func(cmd *cobra.Command, args []string) error {
defer tui.LogFile.Close()

if flags.filename == "" {
return fmt.Errorf("Flag -f (--filename) required")
}

kubeconfigNamespace, restConfig, err := utils.BuildConfigFromFlags("", flags.kubeconfig)
if err != nil {
return fmt.Errorf("rest config: %w", err)
}

clientset, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return fmt.Errorf("clientset: %w", err)
}

client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}

// Initialize our program
tui.P = tea.NewProgram((&tui.ApplyModel{
Ctx: cmd.Context(),
Filename: flags.filename,
Namespace: tui.Namespace{
Contextual: kubeconfigNamespace,
Specified: flags.namespace,
},
Client: client,
K8s: clientset,
}).New())
if _, err := tui.P.Run(); err != nil {
return err
}

return nil
}

cmd := &cobra.Command{
Use: "apply",
Aliases: []string{"ap"},
Short: "Apply Substratus (or any Kubernetes) objects",
Example: ` # Scan *.yaml files looking for manifests to apply.
sub apply ./dir/

# Apply a single manifest file.
sub apply -f manifests.yaml

# Apply a remote manifest.
sub apply -f https://some/manifest.yaml`,
Run: func(cmd *cobra.Command, args []string) {
if err := run(cmd, args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}

defaultKubeconfig := os.Getenv("KUBECONFIG")
if defaultKubeconfig == "" {
defaultKubeconfig = clientcmd.RecommendedHomeFile
}

cmd.Flags().StringVarP(&flags.kubeconfig, "kubeconfig", "", defaultKubeconfig, "")
cmd.Flags().StringVarP(&flags.namespace, "namespace", "n", "", "Namespace of Notebook")
cmd.Flags().StringVarP(&flags.filename, "filename", "f", "", "Manifest file")

return cmd
}
7 changes: 5 additions & 2 deletions internal/cli/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ func deleteCommand() *cobra.Command {
return fmt.Errorf("clientset: %w", err)
}

c := NewClient(clientset, restConfig)
client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}

// Initialize our program
tui.P = tea.NewProgram((&tui.DeleteModel{
Expand All @@ -43,7 +46,7 @@ func deleteCommand() *cobra.Command {
Contextual: kubeconfigNamespace,
Specified: flags.namespace,
},
Client: c,
Client: client,
}).New())
if _, err := tui.P.Run(); err != nil {
return err
Expand Down
7 changes: 5 additions & 2 deletions internal/cli/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ func getCommand() *cobra.Command {
return fmt.Errorf("clientset: %w", err)
}

c := NewClient(clientset, restConfig)
client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}

var scope string
if len(args) > 0 {
Expand All @@ -52,7 +55,7 @@ func getCommand() *cobra.Command {
Scope: scope,
Namespace: namespace,

Client: c,
Client: client,
}).New() /*, tea.WithAltScreen()*/)
if _, err := tui.P.Run(); err != nil {
return err
Expand Down
7 changes: 5 additions & 2 deletions internal/cli/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ func inferCommand() *cobra.Command {

_ = namespace

c := NewClient(clientset, restConfig)
_ = c
client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}
_ = client

// Initialize our program
// TODO: Use a differnt tui-model for different types of Model objects:
Expand Down
61 changes: 5 additions & 56 deletions internal/cli/notebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,66 +39,15 @@ func notebookCommand() *cobra.Command {
return fmt.Errorf("rest config: %w", err)
}

//namespace := "default"
//if flags.namespace != "" {
// namespace = flags.namespace
//} else if kubeconfigNamespace != "" {
// namespace = kubeconfigNamespace
//}

clientset, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return fmt.Errorf("clientset: %w", err)
}

c := NewClient(clientset, restConfig)
//notebooks, err := c.Resource(&apiv1.Notebook{
// TypeMeta: metav1.TypeMeta{
// APIVersion: "substratus.ai/v1",
// Kind: "Notebook",
// },
//})
//if err != nil {
// return fmt.Errorf("resource client: %w", err)
//}

//var obj client.Object
//if flags.resume != "" {
// fetched, err := notebooks.Get(namespace, flags.resume)
// if err != nil {
// return fmt.Errorf("getting notebook: %w", err)
// }
// obj = fetched.(client.Object)
//} else {
// manifest, err := os.ReadFile(flags.filename)
// if err != nil {
// return fmt.Errorf("reading file: %w", err)
// }
// obj, err = client.Decode(manifest)
// if err != nil {
// return fmt.Errorf("decoding: %w", err)
// }
// if obj.GetNamespace() == "" {
// // When there is no .metadata.namespace set in the manifest...
// obj.SetNamespace(namespace)
// } else {
// // TODO: Closer match kubectl behavior here by differentiaing between
// // the short -n and long --namespace flags.
// // See example kubectl error:
// // error: the namespace from the provided object "a" does not match the namespace "b". You must pass '--namespace=a' to perform this operation.
// if flags.namespace != "" && flags.namespace != obj.GetNamespace() {
// // When there is .metadata.namespace set in the manifest and
// // a conflicting -n or --namespace flag...
// return fmt.Errorf("the namespace from the provided object %q does not match the namespace %q from flag", obj.GetNamespace(), flags.namespace)
// }
// }
//}

//nb, err := client.NotebookForObject(obj)
//if err != nil {
// return fmt.Errorf("notebook for object: %w", err)
//}
//nb.Spec.Suspend = ptr.To(false)
client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}

var pOpts []tea.ProgramOption
if flags.fullscreen {
Expand All @@ -119,7 +68,7 @@ func notebookCommand() *cobra.Command {
Contextual: kubeconfigNamespace,
Specified: flags.namespace,
},
Client: c,
Client: client,
K8s: clientset,
}).New(), pOpts...)
if _, err := tui.P.Run(); err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func Command() *cobra.Command {
Short: "Substratus CLI",
}

cmd.AddCommand(applyCommand())
cmd.AddCommand(notebookCommand())
cmd.AddCommand(runCommand())
cmd.AddCommand(getCommand())
Expand Down
7 changes: 6 additions & 1 deletion internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ func runCommand() *cobra.Command {
return fmt.Errorf("clientset: %w", err)
}

client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}

path := "."
if len(args) > 0 {
path = args[0]
Expand All @@ -46,7 +51,7 @@ func runCommand() *cobra.Command {
Contextual: kubeconfigNamespace,
Specified: flags.namespace,
},
Client: NewClient(clientset, restConfig),
Client: client,
K8s: clientset,
}).New())
if _, err := tui.P.Run(); err != nil {
Expand Down
18 changes: 17 additions & 1 deletion internal/cli/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,26 @@ func serveCommand() *cobra.Command {
return fmt.Errorf("clientset: %w", err)
}

client, err := NewClient(clientset, restConfig)
if err != nil {
return fmt.Errorf("client: %w", err)
}

wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting working directory: %w", err)
}

// Initialize our program
tui.P = tea.NewProgram((&tui.ServeModel{
Ctx: cmd.Context(),
Path: wd,
Filename: flags.filename,
Namespace: tui.Namespace{
Contextual: kubeconfigNamespace,
Specified: flags.namespace,
},
Client: NewClient(clientset, restConfig),
Client: client,
K8s: clientset,
}).New())
if _, err := tui.P.Run(); err != nil {
Expand All @@ -59,6 +70,11 @@ func serveCommand() *cobra.Command {
Use: "serve",
Aliases: []string{"srv"},
Short: "Serve a model, open a browser",
Example: ` # Scan *.yaml files looking for a Server object to apply.
sub serve

# Serve a given Server manifest.
sub serve -f manifest.yaml`,
Run: func(cmd *cobra.Command, args []string) {
if err := run(cmd, args); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
33 changes: 19 additions & 14 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

meta "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/fields"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -44,31 +45,31 @@ type Interface interface {
) error
}

func NewClient(inf kubernetes.Interface, cfg *rest.Config) Interface {
return &Client{Interface: inf, Config: cfg}
func NewClient(inf kubernetes.Interface, cfg *rest.Config) (Interface, error) {
// Create a REST mapper that tracks information about the available resources in the cluster.
groupResources, err := restmapper.GetAPIGroupResources(inf.Discovery())
if err != nil {
return nil, err
}
rm := restmapper.NewDiscoveryRESTMapper(groupResources)
return &Client{Interface: inf, Config: cfg, RESTMapper: rm}, nil
}

type Client struct {
kubernetes.Interface
Config *rest.Config
meta.RESTMapper
}

type Resource struct {
*resource.Helper
}

func (c *Client) Resource(obj Object) (*Resource, error) {
// Create a REST mapper that tracks information about the available resources in the cluster.
groupResources, err := restmapper.GetAPIGroupResources(c.Interface.Discovery())
if err != nil {
return nil, err
}
rm := restmapper.NewDiscoveryRESTMapper(groupResources)

// Get some metadata needed to make the REST request.
gvk := obj.GetObjectKind().GroupVersionKind()
gk := schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}
mapping, err := rm.RESTMapping(gk, gvk.Version)
mapping, err := c.RESTMapper.RESTMapping(gk, gvk.Version)
if err != nil {
return nil, err
}
Expand All @@ -80,7 +81,7 @@ func (c *Client) Resource(obj Object) (*Resource, error) {
_ = name

// Create a client specifically for working with the object.
restClient, err := newRestClient(c.Config, mapping.GroupVersionKind.GroupVersion())
restClient, err := newRestClient(c.Config, mapping.GroupVersionKind.GroupVersion(), obj)
if err != nil {
return nil, err
}
Expand All @@ -93,9 +94,13 @@ func (c *Client) Resource(obj Object) (*Resource, error) {
return &Resource{Helper: helper}, nil
}

func newRestClient(restConfig *rest.Config, gv schema.GroupVersion) (rest.Interface, error) {
// restConfig.ContentConfig = resource.UnstructuredPlusDefaultContentConfig()
restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
func newRestClient(restConfig *rest.Config, gv schema.GroupVersion, obj Object) (rest.Interface, error) {
if _, ok := obj.(*unstructured.Unstructured); ok {
restConfig.ContentConfig = resource.UnstructuredPlusDefaultContentConfig()
} else {
restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
}

restConfig.GroupVersion = &gv
if len(gv.Group) == 0 {
restConfig.APIPath = "/api"
Expand Down
Loading