feat(image extender): set image for init container
parent
8cb9da07b1
commit
41e9d691b7
2
go.mod
2
go.mod
|
|
@ -23,7 +23,7 @@ require (
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v1.0.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/net v0.0.0-20200519113804-d87ec0cfa476 // indirect
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
|
||||||
golang.org/x/text v0.3.2
|
golang.org/x/text v0.3.2
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -615,6 +615,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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
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.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/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/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=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
|
@ -839,6 +841,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.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
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.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 h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE=
|
helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE=
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||||
return &podSpec, nil
|
return &podSpec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Deployment) SetImages(ctx context.Context, path string, images map[string]string) error {
|
func (d *Deployment) SetImages(ctx context.Context, path string, spec v1.PodSpec) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -229,8 +229,7 @@ func (d *Deployment) SetImages(ctx context.Context, path string, images map[stri
|
||||||
if !auth {
|
if !auth {
|
||||||
return fmt.Errorf("user is not authorized to patch a deployment")
|
return fmt.Errorf("user is not authorized to patch a deployment")
|
||||||
}
|
}
|
||||||
|
jsonPatch, err := SetImageJsonPatch(spec)
|
||||||
jsonPatch, err := SetImageJsonPatch(images)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
type JsonPatch struct {
|
type JsonPatch struct {
|
||||||
Spec Spec `json:"spec"`
|
Spec Spec `json:"spec"`
|
||||||
|
|
@ -15,8 +18,10 @@ type Template struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImagesSpec struct {
|
type ImagesSpec struct {
|
||||||
SetElementOrders []Element `json:"$setElementOrder/containers"`
|
SetElementOrderContainers []Element `json:"$setElementOrder/containers,omitempty"`
|
||||||
Containers []Element `json:"containers"`
|
SetElementOrderInitContainers []Element `json:"$setElementOrder/initContainers,omitempty"`
|
||||||
|
Containers []Element `json:"containers,omitempty"`
|
||||||
|
InitContainers []Element `json:"initContainers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Element struct {
|
type Element struct {
|
||||||
|
|
@ -25,19 +30,15 @@ type Element struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a json patch string to update PodSpec images
|
// Build a json patch string to update PodSpec images
|
||||||
func SetImageJsonPatch(images map[string]string) (string, error) {
|
func SetImageJsonPatch(spec v1.PodSpec) (string, error) {
|
||||||
elementOrders := make([]Element, 0)
|
|
||||||
containers := make([]Element, 0)
|
|
||||||
for key, value := range images {
|
|
||||||
elementOrders = append(elementOrders, Element{Name: key})
|
|
||||||
containers = append(containers, Element{Name: key, Image: value})
|
|
||||||
}
|
|
||||||
jsonPatch := JsonPatch{
|
jsonPatch := JsonPatch{
|
||||||
Spec: Spec{
|
Spec: Spec{
|
||||||
Template: Template{
|
Template: Template{
|
||||||
Spec: ImagesSpec{
|
Spec: ImagesSpec{
|
||||||
SetElementOrders: elementOrders,
|
SetElementOrderContainers: extractElements(spec.Containers, false),
|
||||||
Containers: containers,
|
Containers: extractElements(spec.Containers, true),
|
||||||
|
SetElementOrderInitContainers: extractElements(spec.InitContainers, false),
|
||||||
|
InitContainers: extractElements(spec.InitContainers, true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -45,3 +46,15 @@ func SetImageJsonPatch(images map[string]string) (string, error) {
|
||||||
bytes, err := json.Marshal(jsonPatch)
|
bytes, err := json.Marshal(jsonPatch)
|
||||||
return string(bytes), err
|
return string(bytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractElements(containers []v1.Container, withImage bool) []Element {
|
||||||
|
elements := make([]Element, 0)
|
||||||
|
for _, c := range containers {
|
||||||
|
if withImage {
|
||||||
|
elements = append(elements, Element{Name: c.Name, Image: c.Image})
|
||||||
|
} else {
|
||||||
|
elements = append(elements, Element{Name: c.Name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSetImageJsonPatch(t *testing.T) {
|
func TestSetImageJsonPatch(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
images map[string]string
|
podSpec v1.PodSpec
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -18,22 +19,23 @@ func TestSetImageJsonPatch(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "simple",
|
name: "simple",
|
||||||
args: args{
|
args: args{
|
||||||
images: map[string]string{"nginx": "nginx:latest"},
|
podSpec: v1.PodSpec{
|
||||||
|
InitContainers: []v1.Container{v1.Container{Image: "busybox:latest", Name: "init"}},
|
||||||
|
Containers: []v1.Container{v1.Container{Image: "nginx:latest", Name: "nginx"}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
want: "",
|
want: `{"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"nginx"}],"$setElementOrder/initContainers":[{"name":"init"}],"containers":[{"image":"nginx:latest","name":"nginx"}],"initContainers":[{"image":"busybox:latest","name":"init"}]}}}}`,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := SetImageJsonPatch(tt.args.images)
|
got, err := SetImageJsonPatch(tt.args.podSpec)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("SetImageJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("SetImageJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
require.JSONEq(t, tt.want, got, "Json strings should be equal")
|
||||||
t.Errorf("SetImageJsonPatch() got = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,5 +152,5 @@ type ContainsPodSpec interface {
|
||||||
GetPodSpec(path string) (*v1.PodSpec, error)
|
GetPodSpec(path string) (*v1.PodSpec, error)
|
||||||
|
|
||||||
// Set Images for a resource
|
// Set Images for a resource
|
||||||
SetImages(ctx context.Context, path string, images map[string]string) error
|
SetImages(ctx context.Context, path string, spec v1.PodSpec) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,29 @@ func (s *SetImageExtender) showSetImageDialog(path string) {
|
||||||
s.App().Content.ShowPage(setImageKey)
|
s.App().Content.ShowPage(setImageKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContainerType string
|
||||||
|
|
||||||
|
var runningContainer = ContainerType("Container")
|
||||||
|
var initContainer = ContainerType("InitContainer")
|
||||||
|
|
||||||
|
type ContainerImage struct {
|
||||||
|
ContainerType ContainerType
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
||||||
f := s.makeStyledForm()
|
f := s.makeStyledForm()
|
||||||
containers, err := s.getImages(sel)
|
podSpec, err := s.getPodSpec(sel)
|
||||||
|
originalImages := getImages(podSpec)
|
||||||
|
formSubmitResult := make(map[string]ContainerImage, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.App().Flash().Err(err)
|
s.App().Flash().Err(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
images := make(map[string]string)
|
for name, containerImage := range originalImages {
|
||||||
for _, container := range *containers {
|
f.AddInputField(name, containerImage.Image, 0, nil, func(changed string) {
|
||||||
f.AddInputField(container.Name, container.Image, 0, nil, func(changed string) {
|
log.Info().Msgf("changed : %v", changed)
|
||||||
images[container.Name] = changed
|
formSubmitResult[name] = ContainerImage{ContainerType: containerImage.ContainerType, Image: changed}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,14 +89,15 @@ func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.setImages(ctx, sel, images); err != nil {
|
podSpecPatch := buildPodSpecPatch(formSubmitResult, originalImages)
|
||||||
|
if err := s.setImages(ctx, sel, podSpecPatch); err != nil {
|
||||||
|
|
||||||
log.Error().Err(err).Msgf("DP %s image update failed", sel)
|
log.Error().Err(err).Msgf("DP %s image update failed", sel)
|
||||||
s.App().Flash().Err(err)
|
s.App().Flash().Err(err)
|
||||||
} else {
|
} else {
|
||||||
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
|
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
f.AddButton("Cancel", func() {
|
f.AddButton("Cancel", func() {
|
||||||
s.dismissDialog()
|
s.dismissDialog()
|
||||||
})
|
})
|
||||||
|
|
@ -92,6 +105,48 @@ func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getImages(podSpec *corev1.PodSpec) map[string]ContainerImage {
|
||||||
|
results := make(map[string]ContainerImage, 0)
|
||||||
|
for _, c := range podSpec.Containers {
|
||||||
|
results[c.Name] = ContainerImage{
|
||||||
|
ContainerType: runningContainer,
|
||||||
|
Image: c.Image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range podSpec.InitContainers {
|
||||||
|
results[c.Name] = ContainerImage{
|
||||||
|
ContainerType: initContainer,
|
||||||
|
Image: c.Image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPodSpecPatch(formImages map[string]ContainerImage, originalImages map[string]ContainerImage) corev1.PodSpec {
|
||||||
|
initContainers := make([]corev1.Container, 0)
|
||||||
|
containers := make([]corev1.Container, 0)
|
||||||
|
for name, containerImage := range formImages {
|
||||||
|
if originalImages[name].Image == containerImage.Image {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
container := corev1.Container{
|
||||||
|
Image: containerImage.Image,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
switch containerImage.ContainerType {
|
||||||
|
case runningContainer:
|
||||||
|
containers = append(containers, container)
|
||||||
|
case initContainer:
|
||||||
|
initContainers = append(initContainers, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := corev1.PodSpec{
|
||||||
|
Containers: containers,
|
||||||
|
InitContainers: initContainers,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SetImageExtender) dismissDialog() {
|
func (s *SetImageExtender) dismissDialog() {
|
||||||
s.App().Content.RemovePage(setImageKey)
|
s.App().Content.RemovePage(setImageKey)
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +162,7 @@ func (s *SetImageExtender) makeStyledForm() *tview.Form {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SetImageExtender) getImages(path string) (*[]corev1.Container, error) {
|
func (s *SetImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) {
|
||||||
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -117,11 +172,10 @@ func (s *SetImageExtender) getImages(path string) (*[]corev1.Container, error) {
|
||||||
return nil, fmt.Errorf("expecting a podSpecable resource for %q", s.GVR())
|
return nil, fmt.Errorf("expecting a podSpecable resource for %q", s.GVR())
|
||||||
}
|
}
|
||||||
podSpec, err := podSpecable.GetPodSpec(path)
|
podSpec, err := podSpecable.GetPodSpec(path)
|
||||||
containers := podSpec.Containers
|
return podSpec, nil
|
||||||
return &containers, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SetImageExtender) setImages(ctx context.Context, path string, images map[string]string) error {
|
func (s *SetImageExtender) setImages(ctx context.Context, path string, spec corev1.PodSpec) error {
|
||||||
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -132,5 +186,5 @@ func (s *SetImageExtender) setImages(ctx context.Context, path string, images ma
|
||||||
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
|
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
|
||||||
}
|
}
|
||||||
|
|
||||||
return deployment.SetImages(ctx, path, images)
|
return deployment.SetImages(ctx, path, spec)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue