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  }