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

Commit

Permalink
Add sub apply cmd (#253)
Browse files Browse the repository at this point in the history
* Add `sub apply` cmd
* Change `-f` flags to operate based on current directory
  • Loading branch information
nstogner authored Oct 11, 2023
1 parent 1dcdd5c commit aa7e9f9
Show file tree
Hide file tree
Showing 16 changed files with 501 additions and 142 deletions.
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

0 comments on commit aa7e9f9

Please sign in to comment.