From 1b48f8560d1a603450dd6cbc7e09c9d7e5b9da46 Mon Sep 17 00:00:00 2001 From: Nick Stogner Date: Tue, 10 Oct 2023 21:19:21 -0400 Subject: [PATCH 1/2] Add sub apply cmd --- internal/cli/apply.go | 93 ++++++++++++++++ internal/cli/delete.go | 7 +- internal/cli/get.go | 7 +- internal/cli/infer.go | 7 +- internal/cli/notebook.go | 61 +---------- internal/cli/root.go | 1 + internal/cli/run.go | 7 +- internal/cli/serve.go | 18 ++- internal/client/client.go | 33 +++--- internal/client/decode_encode.go | 13 ++- internal/tui/apply.go | 176 ++++++++++++++++++++++++++++++ internal/tui/common.go | 18 ++- internal/tui/get.go | 2 +- internal/tui/manifests.go | 181 ++++++++++++++++++++++--------- internal/tui/notebook.go | 7 +- internal/tui/serve.go | 12 +- 16 files changed, 501 insertions(+), 142 deletions(-) create mode 100644 internal/cli/apply.go create mode 100644 internal/tui/apply.go diff --git a/internal/cli/apply.go b/internal/cli/apply.go new file mode 100644 index 00000000..743eb04f --- /dev/null +++ b/internal/cli/apply.go @@ -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 +} diff --git a/internal/cli/delete.go b/internal/cli/delete.go index b576ed6c..75d63ea8 100644 --- a/internal/cli/delete.go +++ b/internal/cli/delete.go @@ -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{ @@ -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 diff --git a/internal/cli/get.go b/internal/cli/get.go index 0db3387c..b95d7a37 100644 --- a/internal/cli/get.go +++ b/internal/cli/get.go @@ -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 { @@ -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 diff --git a/internal/cli/infer.go b/internal/cli/infer.go index b6b30df6..a194da90 100644 --- a/internal/cli/infer.go +++ b/internal/cli/infer.go @@ -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: diff --git a/internal/cli/notebook.go b/internal/cli/notebook.go index a8abf554..58f58807 100644 --- a/internal/cli/notebook.go +++ b/internal/cli/notebook.go @@ -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 { @@ -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 { diff --git a/internal/cli/root.go b/internal/cli/root.go index c4ad1ab5..edbd3180 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -12,6 +12,7 @@ func Command() *cobra.Command { Short: "Substratus CLI", } + cmd.AddCommand(applyCommand()) cmd.AddCommand(notebookCommand()) cmd.AddCommand(runCommand()) cmd.AddCommand(getCommand()) diff --git a/internal/cli/run.go b/internal/cli/run.go index dbcb0861..51247407 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -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] @@ -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 { diff --git a/internal/cli/serve.go b/internal/cli/serve.go index 8e42b850..495d1713 100644 --- a/internal/cli/serve.go +++ b/internal/cli/serve.go @@ -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 { @@ -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) diff --git a/internal/client/client.go b/internal/client/client.go index 8311805a..8df61b47 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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" @@ -44,13 +45,20 @@ 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 { @@ -58,17 +66,10 @@ type Resource struct { } 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 } @@ -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 } @@ -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" diff --git a/internal/client/decode_encode.go b/internal/client/decode_encode.go index a39129fd..617adea4 100644 --- a/internal/client/decode_encode.go +++ b/internal/client/decode_encode.go @@ -1,6 +1,9 @@ package client import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/yaml" ) @@ -10,7 +13,15 @@ func Decode(data []byte) (Object, error) { runtimeObject, gvk, err := decoder.Decode(data, nil, nil) if gvk == nil { - return nil, nil + var obj unstructured.Unstructured + jsonData, err := yaml.YAMLToJSON(data) + if err != nil { + return nil, fmt.Errorf("failed to convert yaml to json: %w", err) + } + if _, _, err := unstructured.UnstructuredJSONScheme.Decode(jsonData, nil, &obj); err != nil { + return nil, fmt.Errorf("failed to decode to unstructured object: %w", err) + } + return &obj, nil } if err != nil { return nil, err diff --git a/internal/tui/apply.go b/internal/tui/apply.go new file mode 100644 index 00000000..4a5054b7 --- /dev/null +++ b/internal/tui/apply.go @@ -0,0 +1,176 @@ +package tui + +import ( + "context" + "fmt" + "log" + + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + + "github.com/substratusai/substratus/internal/client" +) + +type applyObjectKey struct { + schema.GroupVersionKind + types.NamespacedName +} + +type applyObject struct { + object client.Object + status status + error error + spinner spinner.Model +} + +type ApplyModel struct { + // Cancellation + Ctx context.Context + + // Config + Namespace Namespace + Filename string + NoOpenBrowser bool + + // Clients + Client client.Interface + K8s *kubernetes.Clientset + + objects []applyObject + + applying status + + Style lipgloss.Style + + // End times + quitting bool + finalError error +} + +func (m *ApplyModel) New() ApplyModel { + m.Style = appStyle + + return *m +} + +func (m ApplyModel) Init() tea.Cmd { + return findManifests(m.Filename, false) +} + +func (m ApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Printf("MSG: %T", msg) + + var cmds []tea.Cmd + + apply := func(o client.Object, idx int) { + res, err := m.Client.Resource(o) + if err != nil { + m.finalError = fmt.Errorf("resource client: %w", err) + return + } + + cmds = append(cmds, applyCmd(m.Ctx, res, &applyInput{ + Object: o.DeepCopyObject().(client.Object), + index: idx, + })) + } + switch msg := msg.(type) { + case manifestsFoundMsg: + m.applying = inProgress + m.objects = []applyObject{} + for _, o := range msg.manifests { + o = o.DeepCopyObject().(client.Object) + m.Namespace.Set(o) + s := spinner.New(spinner.WithSpinner(spinner.MiniDot), spinner.WithStyle(activeSpinnerStyle)) + m.objects = append(m.objects, applyObject{ + object: o, + status: inProgress, + spinner: s, + }) + cmds = append(cmds, s.Tick) + apply(o, 0) + } + return m, tea.Batch(cmds...) + + case spinner.TickMsg: + for k, o := range m.objects { + if o.spinner.ID() == msg.ID { + var cmd tea.Cmd + o.spinner, cmd = o.spinner.Update(msg) + m.objects[k] = o + return m, cmd + } + } + + case appliedMsg: + ao := m.objects[msg.index] + ao.status = completed + ao.error = msg.err + m.objects[msg.index] = ao + + if msg.index == len(m.objects)-1 { + m.applying = completed + return m, tea.Quit + } + apply(m.objects[msg.index+1].object, msg.index+1) + return m, tea.Batch(cmds...) + + case tea.KeyMsg: + log.Println("Received key msg:", msg.String()) + if msg.String() == "q" { + return m, tea.Quit + } + + case tea.WindowSizeMsg: + m.Style.Width(msg.Width) + + case error: + log.Printf("Error message: %v", msg) + m.finalError = msg + return m, tea.Quit + } + + return m, nil +} + +// View returns a string based on data in the model. That string which will be +// rendered to the terminal. +func (m ApplyModel) View() (v string) { + defer func() { + v = m.Style.Render(v) + }() + + if m.finalError != nil { + v += errorStyle.Width(m.Style.GetWidth()-m.Style.GetHorizontalMargins()-10).Render("Error: "+m.finalError.Error()) + "\n" + return + } + + for _, o := range m.objects { + var indicator string + if o.status != completed { + indicator = o.spinner.View() + } else { + if o.error != nil { + indicator = xMark.String() + } else { + indicator = checkMark.String() + } + } + gvk := o.object.GetObjectKind().GroupVersionKind() + v += fmt.Sprintf("%s %v: %v\n", + indicator, gvk.Kind, + o.object.GetName(), + ) + } + + if m.applying == inProgress { + v += "\nApplying...\n" + v += helpStyle("Press \"q\" to quit") + } + + return v +} diff --git a/internal/tui/common.go b/internal/tui/common.go index 141a637c..67025f8c 100644 --- a/internal/tui/common.go +++ b/internal/tui/common.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "log" + "net/http" "os" "strconv" "strings" + "time" tea "github.com/charmbracelet/bubbletea" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,6 +23,7 @@ import ( var ( P *tea.Program LogFile *os.File + HTTPC = &http.Client{Timeout: 30 * time.Second} ) func init() { @@ -130,14 +133,21 @@ func applyWithUploadCmd(ctx context.Context, res *client.Resource, obj client.Ob type appliedMsg struct { client.Object + index int + err error } -func applyCmd(ctx context.Context, res *client.Resource, obj client.Object) tea.Cmd { +type applyInput struct { + client.Object + index int +} + +func applyCmd(ctx context.Context, res *client.Resource, in *applyInput) tea.Cmd { return func() tea.Msg { - if err := res.Apply(obj, true); err != nil { - return fmt.Errorf("applying: %w", err) + if err := res.Apply(in.Object, true); err != nil { + return appliedMsg{index: in.index, err: err} } - return appliedMsg{Object: obj} + return appliedMsg{Object: in.Object, index: in.index} } } diff --git a/internal/tui/get.go b/internal/tui/get.go index de3f44bb..e1147837 100644 --- a/internal/tui/get.go +++ b/internal/tui/get.go @@ -179,7 +179,7 @@ func (m GetModel) View() (v string) { var groups []objectVersions var lastUnversionedName string - // var longestName int + const longestName = 30 for _, name := range names { o := m.objects[resource.plural][name] diff --git a/internal/tui/manifests.go b/internal/tui/manifests.go index c7097aa8..dec3c584 100644 --- a/internal/tui/manifests.go +++ b/internal/tui/manifests.go @@ -3,7 +3,9 @@ package tui import ( "bytes" "fmt" + "io" "log" + "net/http" "os" "path/filepath" "strings" @@ -16,8 +18,9 @@ import ( ) type manifestsModel struct { - Path string - Filename string + Path string + Filename string + SubstratusOnly bool // Kinds is a list of manifest kinds to include in results, // ordered by preference. @@ -38,9 +41,13 @@ func (m manifestsModel) Active() bool { } func (m manifestsModel) Init() tea.Cmd { + path := m.Path + if m.Filename != "" { + path = m.Filename + } return tea.Sequence( func() tea.Msg { return manifestsInitMsg{} }, - findSubstratusManifests(m.Path, m.Filename), + findManifests(path, m.SubstratusOnly), ) } @@ -57,13 +64,14 @@ func (m manifestsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, func() tea.Msg { return manifestSelectedMsg{obj: msg.obj} } - case substratusManifestsMsg: + case manifestsFoundMsg: m.reading = completed var n int var single client.Object + byKind := groupObjectsByKind(msg.manifests) for _, k := range m.Kinds { - items := msg.manifests[k] + items := byKind[k] if single == nil && len(items) > 0 { single = items[0] } @@ -106,59 +114,125 @@ type manifestSelectedMsg struct { obj client.Object } -type substratusManifestsMsg struct { - manifests map[string][]client.Object +type manifestsFoundMsg struct { + manifests []client.Object +} + +func groupObjectsByKind(objs []client.Object) map[string][]client.Object { + g := make(map[string][]client.Object) + for _, o := range objs { + kind := o.GetObjectKind().GroupVersionKind().Kind + g[kind] = append(g[kind], o) + } + return g } -func findSubstratusManifests(path, filename string) tea.Cmd { +func findManifests(path string, substratusOnly bool) tea.Cmd { return func() tea.Msg { - msg := substratusManifestsMsg{ - manifests: map[string][]client.Object{}, + manifests, err := resolveManifests(path, substratusOnly) + if err != nil { + return fmt.Errorf("resolving manifests: %w", err) } - var fp string - if filename != "" { - fp = filepath.Join(path, filename) - manifest, err := os.ReadFile(fp) + var all []client.Object + for _, manifest := range manifests { + objs, err := manifestToObjects(manifest, substratusOnly) if err != nil { - return fmt.Errorf("reading file: %w", err) - } - if err := manifestToObjects(manifest, msg.manifests); err != nil { - return fmt.Errorf("reading manifests in file: %v: %w", filename, err) - } - } else { - if path == "" { - var err error - path, err = os.Getwd() - if err != nil { - return err - } + return fmt.Errorf("manifest to objects: %w", err) } + all = append(all, objs...) + } + + if len(all) == 0 { + return fmt.Errorf("No manifests found: %v", path) + } + + return manifestsFoundMsg{ + manifests: all, + } + } +} + +func resolveManifests(path string, substratusOnly bool) ([][]byte, error) { + typ, err := determinePathType(path) + if err != nil { + return nil, fmt.Errorf("determining path type: %w", err) + } + + switch typ { + case pathHTTP: + resp, err := HTTPC.Get(path) + if err != nil { + return nil, fmt.Errorf("http: %w", err) + } + defer resp.Body.Close() + + // Check server response + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http: bad status: %s", resp.Status) + } + + manifest, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("http: reading: %w", err) + } + return [][]byte{manifest}, nil + + case pathFile: + manifest, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + return [][]byte{manifest}, nil + case pathDir: + glob := filepath.Join(path, "*.yaml") + matches, err := filepath.Glob(glob) + if err != nil { + return nil, err + } - fp = filepath.Join(path, "*.yaml") - matches, err := filepath.Glob(fp) + var all [][]byte + for _, p := range matches { + manifest, err := os.ReadFile(p) if err != nil { - return err - } - for _, p := range matches { - manifest, err := os.ReadFile(p) - if err != nil { - return fmt.Errorf("reading file: %w", err) - } - - if err := manifestToObjects(manifest, msg.manifests); err != nil { - return fmt.Errorf("reading manifests in file: %v: %w", p, err) - } + return nil, fmt.Errorf("reading file: %w", err) } + all = append(all, manifest) } - if len(msg.manifests) == 0 { - return fmt.Errorf("No manifests found: %v", fp) - } - return msg + return all, nil + + default: + return nil, fmt.Errorf("unrecognized path type: %s", typ) } } -func manifestToObjects(manifest []byte, m map[string][]client.Object) error { +type pathType string + +const ( + pathFile = "file" + pathDir = "dir" + pathHTTP = "http" +) + +func determinePathType(path string) (pathType, error) { + if strings.HasPrefix(path, "http://") || + strings.HasPrefix(path, "https://") { + return pathHTTP, nil + } + fileInfo, err := os.Stat(path) + if err != nil { + return "", err + } + + if fileInfo.IsDir() { + return pathDir, nil + } + + return pathFile, nil +} + +func manifestToObjects(manifest []byte, substratusOnly bool) ([]client.Object, error) { + var m []client.Object split := bytes.Split(manifest, []byte("---\n")) for _, doc := range split { if strings.TrimSpace(string(doc)) == "" { @@ -167,21 +241,22 @@ func manifestToObjects(manifest []byte, m map[string][]client.Object) error { obj, err := client.Decode(doc) if err != nil { - return fmt.Errorf("decoding: %w", err) + return nil, fmt.Errorf("decoding: %w", err) } if obj == nil { - return nil + log.Println("ignoring nil object: %v", doc) + continue } - switch t := obj.(type) { - case *apiv1.Model, *apiv1.Dataset, *apiv1.Server, *apiv1.Notebook: - kind := t.GetObjectKind().GroupVersionKind().Kind - if m[kind] == nil { - m[kind] = make([]client.Object, 0) + if substratusOnly { + switch obj.(type) { + case *apiv1.Model, *apiv1.Dataset, *apiv1.Server, *apiv1.Notebook: + m = append(m, obj) } - m[kind] = append(m[kind], obj) + } else { + m = append(m, obj) } } - return nil + return m, nil } diff --git a/internal/tui/notebook.go b/internal/tui/notebook.go index ea681881..aa46b9ee 100644 --- a/internal/tui/notebook.go +++ b/internal/tui/notebook.go @@ -64,9 +64,10 @@ func (m NotebookModel) cleanupAndQuitCmd() tea.Msg { func (m *NotebookModel) New() NotebookModel { m.manifests = (&manifestsModel{ - Path: m.Path, - Filename: m.Filename, - Kinds: []string{"Notebook", "Model", "Dataset"}, + Path: m.Path, + Filename: m.Filename, + SubstratusOnly: true, + Kinds: []string{"Notebook", "Model", "Dataset"}, }).New() m.upload = (&uploadModel{ Ctx: m.Ctx, diff --git a/internal/tui/serve.go b/internal/tui/serve.go index dfe390e5..02b40c49 100644 --- a/internal/tui/serve.go +++ b/internal/tui/serve.go @@ -22,6 +22,7 @@ type ServeModel struct { // Config Namespace Namespace + Path string Filename string NoOpenBrowser bool @@ -54,7 +55,10 @@ type ServeModel struct { func (m *ServeModel) New() ServeModel { m.manifests = (&manifestsModel{ - Kinds: []string{"Server"}, + Path: m.Path, + Filename: m.Filename, + Kinds: []string{"Server"}, + SubstratusOnly: true, }).New() m.readiness = (&readinessModel{ Ctx: m.Ctx, @@ -107,7 +111,7 @@ func (m ServeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.resource = res m.applying = inProgress - cmds = append(cmds, applyCmd(m.Ctx, m.resource, m.server.DeepCopy())) + cmds = append(cmds, applyCmd(m.Ctx, m.resource, &applyInput{Object: m.server.DeepCopy()})) } switch msg := msg.(type) { @@ -117,6 +121,10 @@ func (m ServeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { apply() case appliedMsg: + if msg.err != nil { + m.finalError = msg.err + break + } m.applying = completed m.server = msg.Object.(*apiv1.Server) From e9e84888620b1eda544b0f0a4c585bd031e2f898 Mon Sep 17 00:00:00 2001 From: Sam Stoelinga Date: Tue, 10 Oct 2023 21:44:10 -0700 Subject: [PATCH 2/2] fix tests --- internal/tui/manifests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/manifests.go b/internal/tui/manifests.go index dec3c584..b9d58710 100644 --- a/internal/tui/manifests.go +++ b/internal/tui/manifests.go @@ -244,7 +244,7 @@ func manifestToObjects(manifest []byte, substratusOnly bool) ([]client.Object, e return nil, fmt.Errorf("decoding: %w", err) } if obj == nil { - log.Println("ignoring nil object: %v", doc) + log.Printf("ignoring nil object: %v", doc) continue }