feat: add error if try to set image on managed pod
remove usage of v1.PodSpec for patch resourcesmine
parent
7665af1c6f
commit
6f9f9ccea5
|
|
@ -220,7 +220,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
|
|||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (d *Deployment) SetImages(ctx context.Context, path string, spec v1.PodSpec) error {
|
||||
func (d *Deployment) SetImages(ctx context.Context, path string, containersPatch map[string]string, initContainersPatch map[string]string) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
|
|
@ -229,7 +229,7 @@ func (d *Deployment) SetImages(ctx context.Context, path string, spec v1.PodSpec
|
|||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a deployment")
|
||||
}
|
||||
jsonPatch, err := GetTemplateJsonPatch(spec)
|
||||
jsonPatch, err := GetTemplateJsonPatch(containersPatch, initContainersPatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
|
|||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (d *DaemonSet) SetImages(ctx context.Context, path string, spec v1.PodSpec) error {
|
||||
func (d *DaemonSet) SetImages(ctx context.Context, path string, containersPatch map[string]string, initContainersPatch map[string]string) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := d.Client().CanI(ns, "apps/v1/daemonset", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
|
|
@ -244,7 +244,7 @@ func (d *DaemonSet) SetImages(ctx context.Context, path string, spec v1.PodSpec)
|
|||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a daemonset")
|
||||
}
|
||||
jsonPatch, err := GetTemplateJsonPatch(spec)
|
||||
jsonPatch, err := GetTemplateJsonPatch(containersPatch, initContainersPatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package dao
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type JsonPatch struct {
|
||||
|
|
@ -30,43 +29,40 @@ type Element struct {
|
|||
}
|
||||
|
||||
// Build a json patch string to update PodSpec images
|
||||
func GetTemplateJsonPatch(spec v1.PodSpec) (string, error) {
|
||||
func GetTemplateJsonPatch(containersPatch map[string]string, initContainersPatch map[string]string) (string, error) {
|
||||
jsonPatch := JsonPatch{
|
||||
Spec: Spec{
|
||||
Template: getPatchPodSpec(spec),
|
||||
Template: getPatchPodSpec(containersPatch, initContainersPatch),
|
||||
},
|
||||
}
|
||||
bytes, err := json.Marshal(jsonPatch)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func GetJsonPatch(spec v1.PodSpec) (string, error) {
|
||||
podSpec := getPatchPodSpec(spec)
|
||||
func GetJsonPatch(containersPatch map[string]string, initContainersPatch map[string]string) (string, error) {
|
||||
podSpec := getPatchPodSpec(containersPatch, initContainersPatch)
|
||||
bytes, err := json.Marshal(podSpec)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func getPatchPodSpec(spec v1.PodSpec) PodSpec {
|
||||
func getPatchPodSpec(containersPatch map[string]string, initContainersPatch map[string]string) PodSpec {
|
||||
elementsOrders, elements := extractElements(containersPatch)
|
||||
initElementsOrders, initElements := extractElements(initContainersPatch)
|
||||
podSpec := PodSpec{
|
||||
Spec: ImagesSpec{
|
||||
SetElementOrderContainers: extractElements(spec.Containers, false),
|
||||
Containers: extractElements(spec.Containers, true),
|
||||
SetElementOrderInitContainers: extractElements(spec.InitContainers, false),
|
||||
InitContainers: extractElements(spec.InitContainers, true),
|
||||
SetElementOrderContainers: elementsOrders,
|
||||
Containers: elements,
|
||||
SetElementOrderInitContainers: initElementsOrders,
|
||||
InitContainers: initElements,
|
||||
},
|
||||
}
|
||||
|
||||
return podSpec
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
func extractElements(containers map[string]string) (elementsOrders []Element, elements []Element) {
|
||||
for name, image := range containers {
|
||||
elementsOrders = append(elementsOrders, Element{Name: name})
|
||||
elements = append(elements, Element{Name: name, Image: image})
|
||||
}
|
||||
return elements
|
||||
return elementsOrders, elements
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,35 +2,31 @@ package dao
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetTemplateJsonPatch(t *testing.T) {
|
||||
type args struct {
|
||||
podSpec v1.PodSpec
|
||||
containers map[string]string
|
||||
initContainers map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
tests := map[string]struct {
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
"simple": {
|
||||
args: args{
|
||||
podSpec: v1.PodSpec{
|
||||
InitContainers: []v1.Container{v1.Container{Image: "busybox:latest", Name: "init"}},
|
||||
Containers: []v1.Container{v1.Container{Image: "nginx:latest", Name: "nginx"}},
|
||||
},
|
||||
initContainers: map[string]string{"init": "busybox:latest"},
|
||||
containers: map[string]string{"nginx": "nginx:latest"},
|
||||
},
|
||||
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,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetTemplateJsonPatch(tt.args.podSpec)
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := GetTemplateJsonPatch(tt.args.containers, tt.args.initContainers)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTemplateJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
@ -42,29 +38,26 @@ func TestGetTemplateJsonPatch(t *testing.T) {
|
|||
|
||||
func TestGetJsonPatch(t *testing.T) {
|
||||
type args struct {
|
||||
podSpec v1.PodSpec
|
||||
containers map[string]string
|
||||
initContainers map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
tests := map[string]struct {
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
"simple": {
|
||||
args: args{
|
||||
podSpec: v1.PodSpec{
|
||||
InitContainers: []v1.Container{v1.Container{Image: "busybox:latest", Name: "init"}},
|
||||
Containers: []v1.Container{v1.Container{Image: "nginx:latest", Name: "nginx"}},
|
||||
},
|
||||
initContainers: map[string]string{"init": "busybox:latest"},
|
||||
containers: map[string]string{"nginx": "nginx:latest"},
|
||||
},
|
||||
want: `{"spec":{"$setElementOrder/containers":[{"name":"nginx"}],"$setElementOrder/initContainers":[{"name":"init"}],"containers":[{"image":"nginx:latest","name":"nginx"}],"initContainers":[{"image":"busybox:latest","name":"init"}]}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetJsonPatch(tt.args.podSpec)
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := GetJsonPatch(tt.args.containers, tt.args.initContainers)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTemplateJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
|
|||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (p *Pod) SetImages(ctx context.Context, path string, spec v1.PodSpec) error {
|
||||
func (p *Pod) SetImages(ctx context.Context, path string, containersPatch map[string]string, initContainersPatch map[string]string) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := p.Client().CanI(ns, "v1/pod", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
|
|
@ -476,7 +476,13 @@ func (p *Pod) SetImages(ctx context.Context, path string, spec v1.PodSpec) error
|
|||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a deployment")
|
||||
}
|
||||
jsonPatch, err := GetJsonPatch(spec)
|
||||
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(containersPatch, initContainersPatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -493,3 +499,21 @@ func (p *Pod) SetImages(ctx context.Context, path string, spec v1.PodSpec) error
|
|||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Pod) isControlled(path string) (*string, bool, error) {
|
||||
pod, err := p.GetInstance(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
references := pod.GetObjectMeta().GetOwnerReferences()
|
||||
if len(references) != 0 && *references[0].Controller == true {
|
||||
return getController(references[0]), true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func getController(reference metav1.OwnerReference) *string {
|
||||
var result string
|
||||
result = fmt.Sprintf("%s/%s", reference.Kind, reference.Name)
|
||||
return &result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {
|
|||
return &podSpec, nil
|
||||
}
|
||||
|
||||
func (s *StatefulSet) SetImages(ctx context.Context, path string, spec v1.PodSpec) error {
|
||||
func (s *StatefulSet) SetImages(ctx context.Context, path string, containersPatch map[string]string, initContainersPatch map[string]string) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulset", []string{client.PatchVerb})
|
||||
if err != nil {
|
||||
|
|
@ -240,7 +240,7 @@ func (s *StatefulSet) SetImages(ctx context.Context, path string, spec v1.PodSpe
|
|||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to patch a statefulset")
|
||||
}
|
||||
jsonPatch, err := GetTemplateJsonPatch(spec)
|
||||
jsonPatch, err := GetTemplateJsonPatch(containersPatch, initContainersPatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,5 +152,5 @@ type ContainsPodSpec interface {
|
|||
GetPodSpec(path string) (*v1.PodSpec, error)
|
||||
|
||||
// Set Images for a resource
|
||||
SetImages(ctx context.Context, path string, spec v1.PodSpec) error
|
||||
SetImages(ctx context.Context, path string, containersPatch map[string]string, initContainersPatch map[string]string) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -68,17 +68,15 @@ func (s *SetImageExtender) showSetImageDialog(path string) {
|
|||
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
||||
f := s.makeStyledForm()
|
||||
podSpec, err := s.getPodSpec(sel)
|
||||
originalImages := getImages(podSpec)
|
||||
formSubmitResult := make(map[string]ContainerImage, 0)
|
||||
containers, initContainers := getImages(podSpec)
|
||||
containersResult := make(map[string]string)
|
||||
initContainersResult := make(map[string]string)
|
||||
if err != nil {
|
||||
s.App().Flash().Err(err)
|
||||
return nil
|
||||
}
|
||||
for name, containerImage := range originalImages {
|
||||
f.AddInputField(name, containerImage.Image, 0, nil, func(changed string) {
|
||||
formSubmitResult[name] = ContainerImage{ContainerType: containerImage.ContainerType, Image: changed}
|
||||
})
|
||||
}
|
||||
addInputField(f, &containers, &containersResult)
|
||||
addInputField(f, &initContainers, &initContainersResult)
|
||||
|
||||
f.AddButton("OK", func() {
|
||||
defer s.dismissDialog()
|
||||
|
|
@ -89,9 +87,7 @@ func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
|||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
||||
defer cancel()
|
||||
podSpecPatch := buildPodSpecPatch(formSubmitResult, originalImages)
|
||||
if err := s.setImages(ctx, sel, podSpecPatch); err != nil {
|
||||
|
||||
if err := s.setImages(ctx, sel, containersResult, initContainersResult); err != nil {
|
||||
log.Error().Err(err).Msgf("PodSpec %s image update failed", sel)
|
||||
s.App().Flash().Err(err)
|
||||
} else {
|
||||
|
|
@ -105,46 +101,28 @@ func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
|
|||
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,
|
||||
}
|
||||
func addInputField(f *tview.Form, containers *map[string]string, containersResult *map[string]string) {
|
||||
for name, image := range *containers {
|
||||
f.AddInputField(name, image, 0, nil, func(changed string) {
|
||||
if changed == image {
|
||||
delete(*containersResult, name)
|
||||
} else {
|
||||
(*containersResult)[name] = changed
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
func getImages(podSpec *corev1.PodSpec) (map[string]string, map[string]string) {
|
||||
containers := make(map[string]string)
|
||||
initContainers := make(map[string]string)
|
||||
for _, c := range podSpec.Containers {
|
||||
containers[c.Name] = c.Image
|
||||
}
|
||||
result := corev1.PodSpec{
|
||||
Containers: containers,
|
||||
InitContainers: initContainers,
|
||||
for _, c := range podSpec.InitContainers {
|
||||
initContainers[c.Name] = c.Image
|
||||
}
|
||||
return result
|
||||
return containers, initContainers
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) dismissDialog() {
|
||||
|
|
@ -175,7 +153,7 @@ func (s *SetImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) {
|
|||
return podSpec, nil
|
||||
}
|
||||
|
||||
func (s *SetImageExtender) setImages(ctx context.Context, path string, spec corev1.PodSpec) error {
|
||||
func (s *SetImageExtender) setImages(ctx context.Context, path string, containersPatch map[string]string, initContainersPatch map[string]string) error {
|
||||
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -186,5 +164,5 @@ func (s *SetImageExtender) setImages(ctx context.Context, path string, spec core
|
|||
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
|
||||
}
|
||||
|
||||
return resourceWPodSpec.SetImages(ctx, path, spec)
|
||||
return resourceWPodSpec.SetImages(ctx, path, containersPatch, initContainersPatch)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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