commit
ca51ce547b
2
go.mod
2
go.mod
|
|
@ -24,7 +24,7 @@ require (
|
|||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476 // indirect
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
|
||||
golang.org/x/text v0.3.2
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -613,6 +613,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
|
@ -837,6 +839,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE=
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
|
|
@ -18,12 +17,13 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
_ Accessor = (*Deployment)(nil)
|
||||
_ Nuker = (*Deployment)(nil)
|
||||
_ Loggable = (*Deployment)(nil)
|
||||
_ Restartable = (*Deployment)(nil)
|
||||
_ Scalable = (*Deployment)(nil)
|
||||
_ Controller = (*Deployment)(nil)
|
||||
_ Accessor = (*Deployment)(nil)
|
||||
_ Nuker = (*Deployment)(nil)
|
||||
_ Loggable = (*Deployment)(nil)
|
||||
_ Restartable = (*Deployment)(nil)
|
||||
_ Scalable = (*Deployment)(nil)
|
||||
_ Controller = (*Deployment)(nil)
|
||||
_ ContainsPodSpec = (*Deployment)(nil)
|
||||
)
|
||||
|
||||
// Deployment represents a deployment K8s resource.
|
||||
|
|
@ -211,6 +211,42 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs
|
|||
return refs, nil
|
||||
}
|
||||
|
||||
func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||
dp, err := d.Load(d.Factory, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
podSpec := dp.Spec.Template.Spec
|
||||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a deployment")
|
||||
}
|
||||
jsonPatch, err := GetTemplateJsonPatch(imageSpecs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dial, err := d.Client().Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dial.AppsV1().Deployments(ns).Patch(
|
||||
ctx,
|
||||
n,
|
||||
types.StrategicMergePatchType,
|
||||
jsonPatch,
|
||||
metav1.PatchOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func hasPVC(spec *v1.PodSpec, name string) bool {
|
||||
for _, v := range spec.Volumes {
|
||||
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == name {
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
_ Accessor = (*DaemonSet)(nil)
|
||||
_ Nuker = (*DaemonSet)(nil)
|
||||
_ Loggable = (*DaemonSet)(nil)
|
||||
_ Restartable = (*DaemonSet)(nil)
|
||||
_ Controller = (*DaemonSet)(nil)
|
||||
_ Accessor = (*DaemonSet)(nil)
|
||||
_ Nuker = (*DaemonSet)(nil)
|
||||
_ Loggable = (*DaemonSet)(nil)
|
||||
_ Restartable = (*DaemonSet)(nil)
|
||||
_ Controller = (*DaemonSet)(nil)
|
||||
_ ContainsPodSpec = (*DaemonSet)(nil)
|
||||
)
|
||||
|
||||
// DaemonSet represents a K8s daemonset.
|
||||
|
|
@ -225,6 +226,42 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs,
|
|||
return refs, nil
|
||||
}
|
||||
|
||||
func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||
ds, err := d.GetInstance(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
podSpec := ds.Spec.Template.Spec
|
||||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := d.Client().CanI(ns, "apps/v1/daemonset", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a daemonset")
|
||||
}
|
||||
jsonPatch, err := GetTemplateJsonPatch(imageSpecs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dial, err := d.Client().Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dial.AppsV1().DaemonSets(ns).Patch(
|
||||
ctx,
|
||||
n,
|
||||
types.StrategicMergePatchType,
|
||||
jsonPatch,
|
||||
metav1.PatchOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type ImageSpec struct {
|
||||
Index int
|
||||
Name, DockerImage string
|
||||
Init bool
|
||||
}
|
||||
|
||||
type ImageSpecs []ImageSpec
|
||||
|
||||
type JsonPatch struct {
|
||||
Spec Spec `json:"spec"`
|
||||
}
|
||||
|
||||
type Spec struct {
|
||||
Template PodSpec `json:"template"`
|
||||
}
|
||||
|
||||
type PodSpec struct {
|
||||
Spec ImagesSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type ImagesSpec struct {
|
||||
SetElementOrderContainers []Element `json:"$setElementOrder/containers,omitempty"`
|
||||
SetElementOrderInitContainers []Element `json:"$setElementOrder/initContainers,omitempty"`
|
||||
Containers []Element `json:"containers,omitempty"`
|
||||
InitContainers []Element `json:"initContainers,omitempty"`
|
||||
}
|
||||
|
||||
type Element struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Build a json patch string to update PodSpec images
|
||||
func GetTemplateJsonPatch(imageSpecs ImageSpecs) ([]byte, error) {
|
||||
jsonPatch := JsonPatch{
|
||||
Spec: Spec{
|
||||
Template: getPatchPodSpec(imageSpecs),
|
||||
},
|
||||
}
|
||||
return json.Marshal(jsonPatch)
|
||||
}
|
||||
|
||||
func GetJsonPatch(imageSpecs ImageSpecs) ([]byte, error) {
|
||||
podSpec := getPatchPodSpec(imageSpecs)
|
||||
return json.Marshal(podSpec)
|
||||
}
|
||||
|
||||
func getPatchPodSpec(imageSpecs ImageSpecs) PodSpec {
|
||||
initElementsOrders, initElements, elementsOrders, elements := extractElements(imageSpecs)
|
||||
podSpec := PodSpec{
|
||||
Spec: ImagesSpec{
|
||||
SetElementOrderInitContainers: initElementsOrders,
|
||||
InitContainers: initElements,
|
||||
SetElementOrderContainers: elementsOrders,
|
||||
Containers: elements,
|
||||
},
|
||||
}
|
||||
return podSpec
|
||||
}
|
||||
|
||||
func extractElements(imageSpecs ImageSpecs) (initElementsOrders []Element, initElements []Element, elementsOrders []Element, elements []Element) {
|
||||
for _, spec := range imageSpecs {
|
||||
if spec.Init {
|
||||
initElementsOrders = append(initElementsOrders, Element{Name: spec.Name})
|
||||
initElements = append(initElements, Element{Name: spec.Name, Image: spec.DockerImage})
|
||||
} else {
|
||||
elementsOrders = append(elementsOrders, Element{Name: spec.Name})
|
||||
elements = append(elements, Element{Name: spec.Name, Image: spec.DockerImage})
|
||||
}
|
||||
}
|
||||
return initElementsOrders, initElements, elementsOrders, elements
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetTemplateJsonPatch(t *testing.T) {
|
||||
type args struct {
|
||||
imageSpecs ImageSpecs
|
||||
}
|
||||
tests := map[string]struct {
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
"simple": {
|
||||
args: args{
|
||||
imageSpecs: ImageSpecs{
|
||||
ImageSpec{
|
||||
Index: 0,
|
||||
Name: "init",
|
||||
DockerImage: "busybox:latest",
|
||||
Init: true,
|
||||
},
|
||||
ImageSpec{
|
||||
Index: 0,
|
||||
Name: "nginx",
|
||||
DockerImage: "nginx:latest",
|
||||
Init: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `{"spec":{"template":{"spec":{"$setElementOrder/initContainers":[{"name":"init"}],"$setElementOrder/containers":[{"name":"nginx"}],"initContainers":[{"image":"busybox:latest","name":"init"}],"containers":[{"image":"nginx:latest","name":"nginx"}]}}}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := GetTemplateJsonPatch(tt.args.imageSpecs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTemplateJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.JSONEq(t, tt.want, string(got), "Json strings should be equal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetJsonPatch(t *testing.T) {
|
||||
type args struct {
|
||||
imageSpecs ImageSpecs
|
||||
}
|
||||
tests := map[string]struct {
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
"simple": {
|
||||
args: args{
|
||||
imageSpecs: ImageSpecs{
|
||||
ImageSpec{
|
||||
Index: 0,
|
||||
Name: "init",
|
||||
DockerImage: "busybox:latest",
|
||||
Init: true,
|
||||
},
|
||||
ImageSpec{
|
||||
Index: 0,
|
||||
Name: "nginx",
|
||||
DockerImage: "nginx:latest",
|
||||
Init: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `{"spec":{"$setElementOrder/initContainers":[{"name":"init"}],"initContainers":[{"image":"busybox:latest","name":"init"}],"$setElementOrder/containers":[{"name":"nginx"}],"containers":[{"image":"nginx:latest","name":"nginx"}]}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := GetJsonPatch(tt.args.imageSpecs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTemplateJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.JSONEq(t, tt.want, string(got), "Json strings should be equal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -18,15 +18,17 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Accessor = (*Pod)(nil)
|
||||
_ Nuker = (*Pod)(nil)
|
||||
_ Loggable = (*Pod)(nil)
|
||||
_ Controller = (*Pod)(nil)
|
||||
_ Accessor = (*Pod)(nil)
|
||||
_ Nuker = (*Pod)(nil)
|
||||
_ Loggable = (*Pod)(nil)
|
||||
_ Controller = (*Pod)(nil)
|
||||
_ ContainsPodSpec = (*Pod)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -455,3 +457,57 @@ func in(ll []string, s string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||
pod, err := p.GetInstance(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
podSpec := pod.Spec
|
||||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := p.Client().CanI(ns, "v1/pod", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a deployment")
|
||||
}
|
||||
if manager, isManaged, err := p.isControlled(path); isManaged {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Unable to set image. This pod is managed by %s. Please set the image on the controller", manager)
|
||||
}
|
||||
jsonPatch, err := GetJsonPatch(imageSpecs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dial, err := p.Client().Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dial.CoreV1().Pods(ns).Patch(
|
||||
ctx,
|
||||
n,
|
||||
types.StrategicMergePatchType,
|
||||
jsonPatch,
|
||||
metav1.PatchOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Pod) isControlled(path string) (string, bool, error) {
|
||||
pod, err := p.GetInstance(path)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
references := pod.GetObjectMeta().GetOwnerReferences()
|
||||
if len(references) != 0 && *references[0].Controller == true {
|
||||
return fmt.Sprintf("%s/%s", references[0].Kind, references[0].Name), true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
|
@ -18,12 +19,13 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
_ Accessor = (*StatefulSet)(nil)
|
||||
_ Nuker = (*StatefulSet)(nil)
|
||||
_ Loggable = (*StatefulSet)(nil)
|
||||
_ Restartable = (*StatefulSet)(nil)
|
||||
_ Scalable = (*StatefulSet)(nil)
|
||||
_ Controller = (*StatefulSet)(nil)
|
||||
_ Accessor = (*StatefulSet)(nil)
|
||||
_ Nuker = (*StatefulSet)(nil)
|
||||
_ Loggable = (*StatefulSet)(nil)
|
||||
_ Restartable = (*StatefulSet)(nil)
|
||||
_ Scalable = (*StatefulSet)(nil)
|
||||
_ Controller = (*StatefulSet)(nil)
|
||||
_ ContainsPodSpec = (*StatefulSet)(nil)
|
||||
)
|
||||
|
||||
// StatefulSet represents a K8s sts.
|
||||
|
|
@ -219,3 +221,39 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref
|
|||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||
sts, err := s.getStatefulSet(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
podSpec := sts.Spec.Template.Spec
|
||||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulset", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a statefulset")
|
||||
}
|
||||
jsonPatch, err := GetTemplateJsonPatch(imageSpecs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dial, err := s.Client().Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dial.AppsV1().StatefulSets(ns).Patch(
|
||||
ctx,
|
||||
n,
|
||||
types.StrategicMergePatchType,
|
||||
jsonPatch,
|
||||
metav1.PatchOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,3 +146,11 @@ type Logger interface {
|
|||
// Logs tails a resource logs.
|
||||
Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error)
|
||||
}
|
||||
|
||||
type ContainsPodSpec interface {
|
||||
// Get PodSpec of a resource
|
||||
GetPodSpec(path string) (*v1.PodSpec, error)
|
||||
|
||||
// Set Images for a resource
|
||||
SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
|
|||
ResourceViewer: NewPortForwardExtender(
|
||||
NewRestartExtender(
|
||||
NewScaleExtender(
|
||||
NewLogsExtender(
|
||||
NewBrowser(gvr),
|
||||
nil,
|
||||
NewSetImageExtender(
|
||||
NewLogsExtender(
|
||||
NewBrowser(gvr),
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestDeploy(t *testing.T) {
|
|||
|
||||
assert.Nil(t, v.Init(makeCtx()))
|
||||
assert.Equal(t, "Deployments", v.Name())
|
||||
assert.Equal(t, 12, len(v.Hints()))
|
||||
assert.Equal(t, 13, len(v.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer {
|
|||
d := DaemonSet{
|
||||
ResourceViewer: NewPortForwardExtender(
|
||||
NewRestartExtender(
|
||||
NewLogsExtender(NewBrowser(gvr), nil),
|
||||
NewSetImageExtender(
|
||||
NewLogsExtender(NewBrowser(gvr), nil),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
|
|||
|
||||
assert.Nil(t, v.Init(makeCtx()))
|
||||
assert.Equal(t, "DaemonSets", v.Name())
|
||||
assert.Equal(t, 13, len(v.Hints()))
|
||||
assert.Equal(t, 14, len(v.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
|
|||
v := view.NewHelp()
|
||||
|
||||
assert.Nil(t, v.Init(ctx))
|
||||
assert.Equal(t, 24, v.GetRowCount())
|
||||
assert.Equal(t, 25, v.GetRowCount())
|
||||
assert.Equal(t, 8, v.GetColumnCount())
|
||||
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
|
||||
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ type Pod struct {
|
|||
func NewPod(gvr client.GVR) ResourceViewer {
|
||||
p := Pod{}
|
||||
p.ResourceViewer = NewPortForwardExtender(
|
||||
NewLogsExtender(NewBrowser(gvr), p.selectedContainer),
|
||||
NewSetImageExtender(
|
||||
NewLogsExtender(NewBrowser(gvr), p.selectedContainer),
|
||||
),
|
||||
)
|
||||
p.SetBindKeysFn(p.bindKeys)
|
||||
p.GetTable().SetEnterFn(p.showContainers)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
|
|||
|
||||
assert.Nil(t, po.Init(makeCtx()))
|
||||
assert.Equal(t, "Pods", po.Name())
|
||||
assert.Equal(t, 23, len(po.Hints()))
|
||||
assert.Equal(t, 24, len(po.Hints()))
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const setImageKey = "setImage"
|
||||
|
||||
// SetImageExtender adds set image extensions
|
||||
type SetImageExtender struct {
|
||||
ResourceViewer
|
||||
}
|
||||
|
||||
type imageFormSpec struct {
|
||||
name, dockerImage, newDockerImage string
|
||||
init bool
|
||||
}
|
||||
|
||||
func (m *imageFormSpec) modified() bool {
|
||||
var newDockerImage = strings.TrimSpace(m.newDockerImage)
|
||||
return newDockerImage != "" && m.dockerImage != newDockerImage
|
||||
}
|
||||
|
||||
func (m *imageFormSpec) imageSpec() dao.ImageSpec {
|
||||
var ret = dao.ImageSpec{
|
||||
Name: m.name,
|
||||
Init: m.init,
|
||||
}
|
||||
|
||||
if m.modified() {
|
||||
ret.DockerImage = strings.TrimSpace(m.newDockerImage)
|
||||
} else {
|
||||
ret.DockerImage = m.dockerImage
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func NewSetImageExtender(r ResourceViewer) ResourceViewer {
|
||||
s := SetImageExtender{ResourceViewer: r}
|
||||
s.bindKeys(s.Actions())
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) bindKeys(aa ui.KeyActions) {
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyI: ui.NewKeyAction("SetImage", s.setImageCmd, true),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
path := s.GetTable().GetSelectedItem()
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Stop()
|
||||
defer s.Start()
|
||||
s.showSetImageDialog(path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) showSetImageDialog(path string) {
|
||||
confirm := tview.NewModalForm("<Set image>", s.makeSetImageForm(path))
|
||||
confirm.SetText(fmt.Sprintf("Set image %s %s", s.GVR(), path))
|
||||
confirm.SetDoneFunc(func(int, string) {
|
||||
s.dismissDialog()
|
||||
})
|
||||
s.App().Content.AddPage(setImageKey, confirm, false, false)
|
||||
s.App().Content.ShowPage(setImageKey)
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
||||
f := s.makeStyledForm()
|
||||
podSpec, err := s.getPodSpec(sel)
|
||||
if err != nil {
|
||||
s.App().Flash().Err(err)
|
||||
return nil
|
||||
}
|
||||
var formContainerLines []imageFormSpec
|
||||
for _, spec := range podSpec.InitContainers {
|
||||
formContainerLines = append(formContainerLines, imageFormSpec{init: true, name: spec.Name, dockerImage: spec.Image})
|
||||
}
|
||||
for _, spec := range podSpec.Containers {
|
||||
formContainerLines = append(formContainerLines, imageFormSpec{init: false, name: spec.Name, dockerImage: spec.Image})
|
||||
}
|
||||
for _, ctn := range formContainerLines {
|
||||
ctnCopy := ctn
|
||||
f.AddInputField(ctn.name, ctn.dockerImage, 0, nil, func(changed string) {
|
||||
ctnCopy.newDockerImage = changed
|
||||
})
|
||||
}
|
||||
|
||||
f.AddButton("OK", func() {
|
||||
defer s.dismissDialog()
|
||||
if err != nil {
|
||||
s.App().Flash().Err(err)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
||||
defer cancel()
|
||||
var imageSpecsModified dao.ImageSpecs
|
||||
for _, v := range formContainerLines {
|
||||
if v.modified() {
|
||||
imageSpecsModified = append(imageSpecsModified, v.imageSpec())
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.setImages(ctx, sel, imageSpecsModified); err != nil {
|
||||
log.Error().Err(err).Msgf("PodSpec %s image update failed", sel)
|
||||
s.App().Flash().Err(err)
|
||||
} else {
|
||||
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
|
||||
}
|
||||
})
|
||||
f.AddButton("Cancel", func() {
|
||||
s.dismissDialog()
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) dismissDialog() {
|
||||
s.App().Content.RemovePage(setImageKey)
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) makeStyledForm() *tview.Form {
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
f.SetButtonsAlign(tview.AlignCenter).
|
||||
SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
|
||||
SetButtonTextColor(tview.Styles.PrimaryTextColor).
|
||||
SetLabelColor(tcell.ColorAqua).
|
||||
SetFieldTextColor(tcell.ColorOrange)
|
||||
return f
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) {
|
||||
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceWPodSpec, ok := res.(dao.ContainsPodSpec)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expecting a resourceWPodSpec resource for %q", s.GVR())
|
||||
}
|
||||
return resourceWPodSpec.GetPodSpec(path)
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) setImages(ctx context.Context, path string, imageSpecs dao.ImageSpecs) error {
|
||||
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceWPodSpec, ok := res.(dao.ContainsPodSpec)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
|
||||
}
|
||||
|
||||
return resourceWPodSpec.SetImages(ctx, path, imageSpecs)
|
||||
}
|
||||
|
|
@ -21,7 +21,9 @@ func NewStatefulSet(gvr client.GVR) ResourceViewer {
|
|||
ResourceViewer: NewPortForwardExtender(
|
||||
NewRestartExtender(
|
||||
NewScaleExtender(
|
||||
NewLogsExtender(NewBrowser(gvr), nil),
|
||||
NewSetImageExtender(
|
||||
NewLogsExtender(NewBrowser(gvr), nil),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) {
|
|||
|
||||
assert.Nil(t, s.Init(makeCtx()))
|
||||
assert.Equal(t, "StatefulSets", s.Name())
|
||||
assert.Equal(t, 11, len(s.Hints()))
|
||||
assert.Equal(t, 12, len(s.Hints()))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue