This repository has been archived by the owner on Jan 19, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 490
/
main.go
282 lines (240 loc) · 10 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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
/*
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"
var logger = service.NewLoggerHelper()
// 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},
SupportsObjectStatus: []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.WithObjectStatus(handleStatus),
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.
logger.Info("octant-sample-plugin is starting, with logger helper")
service.SetupPluginLogger(service.Info)
// OUTPUT: 2021-08-23T11:35:02.714-0500 INFO octant-sample-plugin plugin/logger.go:43 octant-sample-plugin is starting, with logger helper {"timestamp": "2021-08-23T11:35:02.714-0500"}
log.Println("octant-sample-plugin is starting with prefix logger")
// OUTPUT: 2021-08-23T11:35:02.714-0500 INFO octant-sample-plugin plugin/logger.go:43 [INFO] 2021/08/23 11:35:02 octant-sample-plugin is starting with prefix logger
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
}
func handleStatus(request *service.PrintRequest) (plugin.ObjectStatusResponse, error) {
if request.Object == nil {
return plugin.ObjectStatusResponse{}, 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.ObjectStatusResponse{}, err
}
u, err := request.DashboardClient.Get(request.Context(), key)
if err != nil {
return plugin.ObjectStatusResponse{}, err
}
// The plugin can check if the object it requested exists.
if u == nil {
return plugin.ObjectStatusResponse{}, errors.New("object doesn't exist")
}
// Will add object UID to the Resource Viewer properties table and status icon details
return plugin.ObjectStatusResponse{
ObjectStatus: component.PodSummary{
// Status: component.NodeStatus,
Details: []component.Component{
component.NewText("from plugin: " + string(u.GetUID())),
},
Properties: []component.Property{{
Label: "ID (from plugin)",
Value: component.NewText(string(u.GetUID())),
}},
},
}, 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 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
})
}