Skip to content
This repository has been archived by the owner on Jan 19, 2023. It is now read-only.

Commit

Permalink
Enable editing of objects
Browse files Browse the repository at this point in the history
This change adds a preview of an editor for object YAML

Signed-off-by: bryanl <bryanliles@gmail.com>
  • Loading branch information
bryanl committed Apr 29, 2020
1 parent 7ea0fbe commit eb705a0
Show file tree
Hide file tree
Showing 24 changed files with 749 additions and 40 deletions.
2 changes: 1 addition & 1 deletion internal/api/websocket_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const (
pingPeriod = (pongWait * 9) / 10

// maxMessageSize is the maximum message size allowed from peer.
maxMessageSize = 512
maxMessageSize = 2 * 1024 * 1024 // 2MiB
)

// WebsocketClient manages websocket clients.
Expand Down
1 change: 1 addition & 0 deletions internal/modules/overview/overview.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ func (co *Overview) ActionPaths() map[string]action.DispatcherFunc {
octant.NewCordon(co.dashConfig.ObjectStore(), co.dashConfig.ClusterClient()),
octant.NewUncordon(co.dashConfig.ObjectStore(), co.dashConfig.ClusterClient()),
octant.NewCronJobTrigger(co.dashConfig.ObjectStore(), co.dashConfig.ClusterClient()),
octant.NewObjectUpdaterDispatcher(co.dashConfig.ObjectStore()),
}

return dispatchers.ToActionPaths()
Expand Down
2 changes: 1 addition & 1 deletion internal/modules/overview/yamlviewer/yamlviewer.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func new(object runtime.Object) (*yamlViewer, error) {

// ToComponent converts the YAMLViewer to a component.
func (yv *yamlViewer) ToComponent() (*component.Editor, error) {
y := component.NewEditor(component.TitleFromString("YAML"), "", true)
y := component.NewEditor(component.TitleFromString("YAML"), "", false)
if err := y.SetValueFromObject(yv.object); err != nil {
return nil, errors.Wrap(err, "add YAML data")
}
Expand Down
7 changes: 4 additions & 3 deletions internal/modules/overview/yamlviewer/yamlviewer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ package yamlviewer
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/vmware-tanzu/octant/internal/testutil"
"github.com/vmware-tanzu/octant/pkg/view/component"

corev1 "k8s.io/api/core/v1"
Expand All @@ -23,7 +23,8 @@ func Test_ToComponent(t *testing.T) {
require.NoError(t, err)

data := "---\nmetadata:\n creationTimestamp: null\nspec:\n containers: null\nstatus: {}\n"
expected := component.NewEditor(component.TitleFromString("YAML"), data, true)
expected := component.NewEditor(component.TitleFromString("YAML"), data, false)
require.NoError(t, expected.SetValueFromObject(object))

assert.Equal(t, expected, got)
testutil.AssertJSONEqual(t, expected, got)
}
17 changes: 17 additions & 0 deletions internal/octant/actions.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package octant

import (
"time"

"github.com/vmware-tanzu/octant/pkg/action"
)

const (
ActionDeleteObject = "action.octant.dev/deleteObject"
ActionOverviewCordon = "action.octant.dev/cordon"
Expand All @@ -8,4 +14,15 @@ const (
ActionOverviewCronjob = "action.octant.dev/cronJob"
ActionOverviewServiceEditor = "action.octant.dev/serviceEditor"
ActionDeploymentConfiguration = "action.octant.dev/deploymentConfiguration"
ActionUpdateObject = "action.octant.dev/update"
)

func sendAlert(alerter action.Alerter, alertType action.AlertType, message string, expiration *time.Time) {
alert := action.Alert{
Type: alertType,
Message: message,
Expiration: expiration,
}

alerter.SendAlert(alert)
}
122 changes: 122 additions & 0 deletions internal/octant/object_updater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2020 the Octant contributors. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*
*/

package octant

import (
"context"
"fmt"
"strings"
"time"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/vmware-tanzu/octant/internal/log"
"github.com/vmware-tanzu/octant/internal/util/kubernetes"
"github.com/vmware-tanzu/octant/pkg/action"
"github.com/vmware-tanzu/octant/pkg/store"
)

// ObjectUpdateFromPayload loads an object from the payload.
// The object source in YAML format should exist in the `update` key.
func ObjectUpdateFromPayload(payload action.Payload) (*unstructured.Unstructured, error) {
s, err := payload.String("update")
if err != nil {
return nil, fmt.Errorf("read object source from payload: %w", err)
}

object, err := kubernetes.ReadObject(strings.NewReader(s))
if err != nil {
return nil, fmt.Errorf("read object from payload: %w", err)
}

return object, nil
}

type ObjectUpdaterDispatcherOption func(dispatcher *ObjectUpdaterDispatcher)

// ObjectUpdaterDispatcher is an action that updates an object.
type ObjectUpdaterDispatcher struct {
store store.Store
objectFromPayload func(payload action.Payload) (*unstructured.Unstructured, error)
}

var _ action.Dispatcher = &ObjectUpdaterDispatcher{}

// NewObjectUpdaterDispatcher creates an instance of ObjectUpdaterDispatcher.
func NewObjectUpdaterDispatcher(objectStore store.Store, options ...ObjectUpdaterDispatcherOption) *ObjectUpdaterDispatcher {
o := ObjectUpdaterDispatcher{
store: objectStore,
objectFromPayload: ObjectUpdateFromPayload,
}

for _, option := range options {
option(&o)
}

return &o
}

// ActionName returns the action name this dispatcher responds to.
func (o ObjectUpdaterDispatcher) ActionName() string {
return ActionUpdateObject
}

// Handle updates an object using a payload if possible.
func (o ObjectUpdaterDispatcher) Handle(ctx context.Context, alerter action.Alerter, payload action.Payload) error {
logger := log.From(ctx)
expiration := time.Now().Add(10 * time.Second)

object, err := o.objectFromPayload(payload)
if err != nil {
sendAlert(
alerter,
action.AlertTypeError,
fmt.Sprintf("load object from payload: %v", err.Error()),
&expiration)
return nil
}

key, _ := store.KeyFromPayload(payload)
err = o.store.Update(ctx, key, func(u *unstructured.Unstructured) error {
if object.GetAPIVersion() != u.GetAPIVersion() {
return fmt.Errorf("object API version cannot be updated")
}
if object.GetKind() != u.GetKind() {
return fmt.Errorf("object kind cannot be updated")
}
if object.GetName() != u.GetName() {
return fmt.Errorf("object name cannot be updated")
}

delete(object.Object, "status")

for k := range object.Object {
u.Object[k] = object.Object[k]
}
return nil
})

if err != nil {
sendAlert(
alerter,
action.AlertTypeError,
fmt.Sprintf("update object: %s", err.Error()),
&expiration)

logger.WithErr(err).Errorf("update object")
return nil
}

successMessage := fmt.Sprintf("Updated %s (%s) %s in %s",
object.GetKind(),
object.GetAPIVersion(),
object.GetName(),
object.GetNamespace())
sendAlert(alerter, action.AlertTypeInfo, successMessage, &expiration)

return nil
}
178 changes: 178 additions & 0 deletions internal/octant/object_updater_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright (c) 2020 the Octant contributors. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*
*/

package octant

import (
"context"
"fmt"
"testing"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/vmware-tanzu/octant/internal/testutil"
"github.com/vmware-tanzu/octant/internal/util/kubernetes"
"github.com/vmware-tanzu/octant/pkg/action"
actionFake "github.com/vmware-tanzu/octant/pkg/action/fake"
"github.com/vmware-tanzu/octant/pkg/store"
storeFake "github.com/vmware-tanzu/octant/pkg/store/fake"
)

func TestObjectUpdateFromPayload(t *testing.T) {
pod := testutil.ToUnstructured(t, testutil.CreatePod("pod"))
podS, err := kubernetes.SerializeToString(pod)
require.NoError(t, err)

tests := []struct {
name string
payload action.Payload
wanted *unstructured.Unstructured
wantErr bool
}{
{
name: "in general",
payload: action.Payload{
"update": podS,
},
wanted: pod,
},
{
name: "no update in payload",
payload: action.Payload{},
wantErr: true,
},
{
name: "upload is not yaml",
payload: action.Payload{
"update": "<<<",
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := ObjectUpdateFromPayload(test.payload)
if test.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, test.wanted, actual)
})
}
}

func TestObjectUpdaterDispatcher_Handle(t *testing.T) {
pod := testutil.ToUnstructured(t, testutil.CreatePod("pod"))
podKey, err := store.KeyFromObject(pod)
require.NoError(t, err)
podPayload := action.Payload{
"namespace": pod.GetNamespace(),
"apiVersion": pod.GetAPIVersion(),
"kind": pod.GetKind(),
"name": pod.GetName(),
}

tests := []struct {
name string
payload action.Payload
objectFromPayload func(action.Payload) (*unstructured.Unstructured, error)
initStore func(ctrl *gomock.Controller) *storeFake.MockStore
initAlerter func(ctrl *gomock.Controller) *actionFake.MockAlerter
wantErr bool
}{
{
name: "in general",
payload: podPayload,
objectFromPayload: func(payload action.Payload) (*unstructured.Unstructured, error) {
return pod, nil
},
initStore: func(ctrl *gomock.Controller) *storeFake.MockStore {
objectStore := storeFake.NewMockStore(ctrl)
objectStore.EXPECT().
Update(gomock.Any(), podKey, gomock.Any()).Return(nil)

return objectStore
},
initAlerter: func(ctrl *gomock.Controller) *actionFake.MockAlerter {
alerter := actionFake.NewMockAlerter(ctrl)
alerter.EXPECT().
SendAlert(gomock.Any()).
DoAndReturn(func(alert action.Alert) {
require.Equal(t, action.AlertTypeInfo, alert.Type)
})
return alerter
},
},
{
name: "unable to load object",
payload: podPayload,
objectFromPayload: func(payload action.Payload) (*unstructured.Unstructured, error) {
return nil, fmt.Errorf("error")
},
initStore: func(ctrl *gomock.Controller) *storeFake.MockStore {
objectStore := storeFake.NewMockStore(ctrl)
return objectStore
},
initAlerter: func(ctrl *gomock.Controller) *actionFake.MockAlerter {
alerter := actionFake.NewMockAlerter(ctrl)
alerter.EXPECT().
SendAlert(gomock.Any()).
DoAndReturn(func(alert action.Alert) {
require.Equal(t, action.AlertTypeError, alert.Type)
})
return alerter
},
},
{
name: "update failed",
payload: podPayload,
objectFromPayload: func(payload action.Payload) (*unstructured.Unstructured, error) {
return pod, nil
},
initStore: func(ctrl *gomock.Controller) *storeFake.MockStore {
objectStore := storeFake.NewMockStore(ctrl)
objectStore.EXPECT().
Update(gomock.Any(), podKey, gomock.Any()).Return(fmt.Errorf("error"))

return objectStore
},
initAlerter: func(ctrl *gomock.Controller) *actionFake.MockAlerter {
alerter := actionFake.NewMockAlerter(ctrl)
alerter.EXPECT().
SendAlert(gomock.Any()).
DoAndReturn(func(alert action.Alert) {
require.Equal(t, action.AlertTypeError, alert.Type)
})
return alerter
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()

ctrl := gomock.NewController(t)
defer ctrl.Finish()

objectStore := test.initStore(ctrl)
alerter := test.initAlerter(ctrl)

o := NewObjectUpdaterDispatcher(objectStore,
func(dispatcher *ObjectUpdaterDispatcher) {
dispatcher.objectFromPayload = test.objectFromPayload
})
err := o.Handle(ctx, alerter, test.payload)
if test.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
Loading

0 comments on commit eb705a0

Please sign in to comment.