github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/kubernetes/client/diff.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "os" 6 "os/exec" 7 "regexp" 8 "strings" 9 10 "github.com/Masterminds/semver" 11 12 "github.com/grafana/tanka/pkg/kubernetes/manifest" 13 ) 14 15 // DiffServerSide takes the desired state and computes the differences server-side, returning them in `diff(1)` format 16 func (k Kubectl) DiffServerSide(data manifest.List) (*string, error) { 17 return k.diff(data, true) 18 } 19 20 // ValidateServerSide takes the desired state and computes the differences, returning them in `diff(1)` format 21 // It also validates that manifests are valid server-side, but still returns the client-side diff 22 func (k Kubectl) ValidateServerSide(data manifest.List) (*string, error) { 23 if _, diffErr := k.diff(data, true); diffErr != nil { 24 return nil, diffErr 25 } 26 return k.diff(data, false) 27 } 28 29 // DiffClientSide takes the desired state and computes the differences, returning them in `diff(1)` format 30 func (k Kubectl) DiffClientSide(data manifest.List) (*string, error) { 31 return k.diff(data, false) 32 } 33 34 func (k Kubectl) diff(data manifest.List, serverSide bool) (*string, error) { 35 fw := FilterWriter{filters: []*regexp.Regexp{regexp.MustCompile(`exit status \d`)}} 36 37 args := []string{"-f", "-"} 38 if serverSide { 39 args = append(args, "--server-side", "--force-conflicts") 40 if k.info.ClientVersion.GreaterThan(semver.MustParse("1.19.0")) { 41 args = append(args, "--field-manager=tanka") 42 } 43 } 44 cmd := k.ctl("diff", args...) 45 46 raw := bytes.Buffer{} 47 // If using an external diff tool, let it keep the parent's stdout 48 if os.Getenv("KUBECTL_INTERACTIVE_DIFF") != "" { 49 cmd.Stdout = os.Stdout 50 } else { 51 cmd.Stdout = &raw 52 } 53 cmd.Stderr = &fw 54 cmd.Stdin = strings.NewReader(data.String()) 55 err := cmd.Run() 56 if diffErr := parseDiffErr(err, fw.buf, k.Info().ClientVersion); diffErr != nil { 57 return nil, diffErr 58 } 59 60 s := raw.String() 61 if s == "" { 62 return nil, nil 63 } 64 65 return &s, nil 66 } 67 68 // parseDiffErr handles the exit status code of `kubectl diff`. It returns err 69 // when an error happened, nil otherwise. 70 // "Differences found (exit status 1)" is not an error. 71 // 72 // kubectl >= 1.18: 73 // 0: no error, no differences 74 // 1: differences found 75 // >1: error 76 // 77 // kubectl < 1.18: 78 // 0: no error, no differences 79 // 1: error OR differences found 80 func parseDiffErr(err error, stderr string, version *semver.Version) error { 81 exitErr, ok := err.(*exec.ExitError) 82 if !ok { 83 // this error is not kubectl related 84 return err 85 } 86 87 // internal kubectl error 88 if exitErr.ExitCode() != 1 { 89 return err 90 } 91 92 // before 1.18 "exit status 1" meant error as well ... so we need to check stderr 93 if version.LessThan(semver.MustParse("1.18.0")) && stderr != "" { 94 return err 95 } 96 97 // differences found is not an error 98 return nil 99 }