feat: add error if try to set image on managed pod

remove usage of v1.PodSpec for patch resources
mine
Antoine Meausoone 2020-09-28 13:55:50 +02:00
parent 7665af1c6f
commit 6f9f9ccea5
12 changed files with 96 additions and 105 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()))
}

View File

@ -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))

View File

@ -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...

View File

@ -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)
}

View File

@ -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()))
}