forked from vmware-archive/octant
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
236 lines (201 loc) · 8.21 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/*
Copyright (c) 2019 the Octant contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"fmt"
"log"
"time"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/vmware-tanzu/octant/pkg/action"
"github.com/vmware-tanzu/octant/pkg/navigation"
"github.com/vmware-tanzu/octant/pkg/plugin"
"github.com/vmware-tanzu/octant/pkg/plugin/service"
"github.com/vmware-tanzu/octant/pkg/store"
"github.com/vmware-tanzu/octant/pkg/view/component"
"github.com/vmware-tanzu/octant/pkg/view/flexlayout"
)
var pluginName = "plugin-name"
const pluginActionName = "action.octant.dev/example"
// This is a sample plugin showing the features of Octant's plugin API.
func main() {
// Remove the prefix from the go logger since Octant will print logs with timestamps.
log.SetPrefix("")
// This plugin is interested in Pods
podGVK := schema.GroupVersionKind{Version: "v1", Kind: "Pod"}
// Tell Octant to call this plugin when printing configuration or tabs for Pods
capabilities := &plugin.Capabilities{
SupportsPrinterConfig: []schema.GroupVersionKind{podGVK},
SupportsTab: []schema.GroupVersionKind{podGVK},
ActionNames: []string{pluginActionName},
IsModule: true,
}
// Set up what should happen when Octant calls this plugin.
options := []service.PluginOption{
service.WithPrinter(handlePrint),
service.WithTabPrinter(handleTab),
service.WithNavigation(handleNavigation, initRoutes),
service.WithActionHandler(handleAction),
}
// Use the plugin service helper to register this plugin.
p, err := service.Register(pluginName, "a description", capabilities, options...)
if err != nil {
log.Fatal(err)
}
// The plugin can log and the log messages will show up in Octant.
log.Printf("octant-sample-plugin is starting")
p.Serve()
}
// handleTab is called when Octant wants to print a tab for an object.
func handleTab(request *service.PrintRequest) (plugin.TabResponse, error) {
if request.Object == nil {
return plugin.TabResponse{}, errors.New("object is nil")
}
// Octant uses flex layouts to display information. It's a flexible
// grid. A flex layout is composed of multiple section. Each section
// can contain multiple components. Components are displayed given
// a width. In the case below, the width is half of the visible space.
// Create sections to separate your components as each section will
// start a new row.
layout := flexlayout.New()
section := layout.AddSection()
// Octant contains a library of components that can be used to display content.
// This example uses markdown text.
contents := component.NewMarkdownText("content from a *plugin*")
err := section.Add(contents, component.WidthHalf)
if err != nil {
return plugin.TabResponse{}, err
}
// In this example, this plugin will tell Octant to create a new
// tab when showing pods. This tab's name will be "Extra Pod Details".
tab := component.NewTabWithContents(*layout.ToComponent("Extra Pod Details"))
return plugin.TabResponse{Tab: tab}, nil
}
// handlePrint is called when Octant wants to print an object.
func handlePrint(request *service.PrintRequest) (plugin.PrintResponse, error) {
if request.Object == nil {
return plugin.PrintResponse{}, errors.Errorf("object is nil")
}
// load an object from the cluster and use that object to create a response.
// Octant has a helper function to generate a key from an object. The key
// is used to find the object in the cluster.
key, err := store.KeyFromObject(request.Object)
if err != nil {
return plugin.PrintResponse{}, err
}
u, err := request.DashboardClient.Get(request.Context(), key)
if err != nil {
return plugin.PrintResponse{}, err
}
// The plugin can check if the object it requested exists.
if u == nil {
return plugin.PrintResponse{}, errors.New("object doesn't exist")
}
// Octant has a component library that can be used to build content for a plugin.
// In this case, the plugin is creating a card.
podCard := component.NewCard(component.TitleFromString(fmt.Sprintf("Extra Output for %s", u.GetName())))
podCard.SetBody(component.NewMarkdownText("This output was generated from _octant-sample-plugin_"))
msg := fmt.Sprintf("update from plugin at %s", time.Now().Format(time.RFC3339))
// When printing an object, you can create multiple types of content. In this
// example, the plugin is:
//
// * adding a field to the configuration section for this object.
// * adding a field to the status section for this object.
// * create a new piece of content that will be embedded in the
// summary section for the component.
return plugin.PrintResponse{
Config: []component.SummarySection{
{Header: "from-plugin", Content: component.NewText(msg)},
},
Status: []component.SummarySection{
{Header: "from-plugin", Content: component.NewText(msg)},
},
Items: []component.FlexLayoutItem{
{
Width: component.WidthHalf,
View: podCard,
},
},
}, nil
}
// handleNavigation creates a navigation tree for this plugin. Navigation is dynamic and will
// be called frequently from Octant. Navigation is a tree of `Navigation` structs.
// The plugin can use whatever paths it likes since these paths can be namespaced to the
// the plugin.
func handleNavigation(request *service.NavigationRequest) (navigation.Navigation, error) {
return navigation.Navigation{
Title: "Sample Plugin",
Path: request.GeneratePath(),
Children: []navigation.Navigation{
{
Title: "Nested Once",
Path: request.GeneratePath("nested-once"),
IconName: "folder",
Children: []navigation.Navigation{
{
Title: "Nested Twice",
Path: request.GeneratePath("nested-once", "nested-twice"),
IconName: "folder",
},
},
},
},
IconName: "cloud",
}, nil
}
// handleAction creates an action handler for this plugin. Actions send
// a payload which are used to execute some task
func handleAction(request *service.ActionRequest) error {
actionValue, err := request.Payload.String("action")
if err != nil {
return err
}
if actionValue == pluginActionName {
// Sending an alert needs a clientID from the request context
alert := action.CreateAlert(action.AlertTypeInfo, fmt.Sprintf("My client ID is: %s", request.ClientState.ClientID()), action.DefaultAlertExpiration)
request.DashboardClient.SendAlert(request.Context(), request.ClientState.ClientID(), alert)
}
return nil
}
// initRoutes routes for this plugin. In this example, there is a global catch all route
// that will return the content for every single path.
func initRoutes(router *service.Router) {
gen := func(name, accessor, requestPath string) component.Component {
cardBody := component.NewText(fmt.Sprintf("hello from plugin: path %s", requestPath))
card := component.NewCard(component.TitleFromString(fmt.Sprintf("My Card - %s", name)))
card.SetBody(cardBody)
form := component.Form{Fields: []component.FormField{
component.NewFormFieldHidden("action", pluginActionName),
}}
testButton := component.Action{
Name: "Test Button",
Title: "Test Button",
Form: form,
}
card.AddAction(testButton)
cardList := component.NewCardList(name)
cardList.AddCard(*card)
cardList.SetAccessor(accessor)
return cardList
}
router.HandleFunc("*", func(request service.Request) (component.ContentResponse, error) {
// For each page, generate two tabs with a some content.
component1 := gen("Tab 1", "tab1", request.Path())
component2 := gen("Tab 2", "tab2", request.Path())
// Illustrate using dropdowns and links for breadcrumbs
items := make([]component.DropdownItemConfig, 0)
dropdown := component.NewDropdown("test", component.DropdownLink, "action", items...)
dropdown.AddDropdownItem("first", component.Url, "Nested Once", "nested-once", "")
dropdown.AddDropdownItem("second", component.Url, "Nested Twice", "nested-once/nested-twice", "")
dropdown.SetTitle(append([]component.TitleComponent{}, component.NewLink("", "Dropdown", "/url")))
var title []component.TitleComponent
title = component.Title(dropdown)
title = append(title, component.NewLink("", "Example Link", "link"))
title = append(title, component.NewText("Example"))
contentResponse := component.NewContentResponse(title)
contentResponse.Add(component1, component2)
return *contentResponse, nil
})
}