diff.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package diff
  14. import (
  15. "fmt"
  16. "io"
  17. "io/ioutil"
  18. "os"
  19. "path/filepath"
  20. "github.com/jonboulle/clockwork"
  21. "github.com/spf13/cobra"
  22. "k8s.io/apimachinery/pkg/api/errors"
  23. "k8s.io/apimachinery/pkg/api/meta"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  26. "k8s.io/apimachinery/pkg/runtime"
  27. "k8s.io/apimachinery/pkg/types"
  28. "k8s.io/cli-runtime/pkg/genericclioptions"
  29. "k8s.io/cli-runtime/pkg/resource"
  30. "k8s.io/client-go/discovery"
  31. "k8s.io/client-go/dynamic"
  32. "k8s.io/klog"
  33. "k8s.io/kubernetes/pkg/kubectl"
  34. "k8s.io/kubernetes/pkg/kubectl/cmd/apply"
  35. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  36. "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
  37. "k8s.io/kubernetes/pkg/kubectl/scheme"
  38. "k8s.io/kubernetes/pkg/kubectl/util/i18n"
  39. "k8s.io/kubernetes/pkg/kubectl/util/templates"
  40. "k8s.io/utils/exec"
  41. "sigs.k8s.io/yaml"
  42. )
  43. var (
  44. diffLong = templates.LongDesc(i18n.T(`
  45. Diff configurations specified by filename or stdin between the current online
  46. configuration, and the configuration as it would be if applied.
  47. Output is always YAML.
  48. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own
  49. diff command. By default, the "diff" command available in your path will be
  50. run with "-u" (unicode) and "-N" (treat new files as empty) options.`))
  51. diffExample = templates.Examples(i18n.T(`
  52. # Diff resources included in pod.json.
  53. kubectl diff -f pod.json
  54. # Diff file read from stdin
  55. cat service.yaml | kubectl diff -f -`))
  56. )
  57. // Number of times we try to diff before giving-up
  58. const maxRetries = 4
  59. type DiffOptions struct {
  60. FilenameOptions resource.FilenameOptions
  61. ServerSideApply bool
  62. ForceConflicts bool
  63. OpenAPISchema openapi.Resources
  64. DiscoveryClient discovery.DiscoveryInterface
  65. DynamicClient dynamic.Interface
  66. DryRunVerifier *apply.DryRunVerifier
  67. CmdNamespace string
  68. EnforceNamespace bool
  69. Builder *resource.Builder
  70. Diff *DiffProgram
  71. }
  72. func validateArgs(cmd *cobra.Command, args []string) error {
  73. if len(args) != 0 {
  74. return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
  75. }
  76. return nil
  77. }
  78. func NewDiffOptions(ioStreams genericclioptions.IOStreams) *DiffOptions {
  79. return &DiffOptions{
  80. Diff: &DiffProgram{
  81. Exec: exec.New(),
  82. IOStreams: ioStreams,
  83. },
  84. }
  85. }
  86. func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
  87. options := NewDiffOptions(streams)
  88. cmd := &cobra.Command{
  89. Use: "diff -f FILENAME",
  90. DisableFlagsInUseLine: true,
  91. Short: i18n.T("Diff live version against would-be applied version"),
  92. Long: diffLong,
  93. Example: diffExample,
  94. Run: func(cmd *cobra.Command, args []string) {
  95. cmdutil.CheckErr(options.Complete(f, cmd))
  96. cmdutil.CheckErr(validateArgs(cmd, args))
  97. cmdutil.CheckErr(options.Run())
  98. },
  99. }
  100. usage := "contains the configuration to diff"
  101. cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
  102. cmdutil.AddServerSideApplyFlags(cmd)
  103. return cmd
  104. }
  105. // DiffProgram finds and run the diff program. The value of
  106. // KUBECTL_EXTERNAL_DIFF environment variable will be used a diff
  107. // program. By default, `diff(1)` will be used.
  108. type DiffProgram struct {
  109. Exec exec.Interface
  110. genericclioptions.IOStreams
  111. }
  112. func (d *DiffProgram) getCommand(args ...string) exec.Cmd {
  113. diff := ""
  114. if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" {
  115. diff = envDiff
  116. } else {
  117. diff = "diff"
  118. args = append([]string{"-u", "-N"}, args...)
  119. }
  120. cmd := d.Exec.Command(diff, args...)
  121. cmd.SetStdout(d.Out)
  122. cmd.SetStderr(d.ErrOut)
  123. return cmd
  124. }
  125. // Run runs the detected diff program. `from` and `to` are the directory to diff.
  126. func (d *DiffProgram) Run(from, to string) error {
  127. return d.getCommand(from, to).Run()
  128. }
  129. // Printer is used to print an object.
  130. type Printer struct{}
  131. // Print the object inside the writer w.
  132. func (p *Printer) Print(obj runtime.Object, w io.Writer) error {
  133. if obj == nil {
  134. return nil
  135. }
  136. data, err := yaml.Marshal(obj)
  137. if err != nil {
  138. return err
  139. }
  140. _, err = w.Write(data)
  141. return err
  142. }
  143. // DiffVersion gets the proper version of objects, and aggregate them into a directory.
  144. type DiffVersion struct {
  145. Dir *Directory
  146. Name string
  147. }
  148. // NewDiffVersion creates a new DiffVersion with the named version.
  149. func NewDiffVersion(name string) (*DiffVersion, error) {
  150. dir, err := CreateDirectory(name)
  151. if err != nil {
  152. return nil, err
  153. }
  154. return &DiffVersion{
  155. Dir: dir,
  156. Name: name,
  157. }, nil
  158. }
  159. func (v *DiffVersion) getObject(obj Object) (runtime.Object, error) {
  160. switch v.Name {
  161. case "LIVE":
  162. return obj.Live(), nil
  163. case "MERGED":
  164. return obj.Merged()
  165. }
  166. return nil, fmt.Errorf("Unknown version: %v", v.Name)
  167. }
  168. // Print prints the object using the printer into a new file in the directory.
  169. func (v *DiffVersion) Print(obj Object, printer Printer) error {
  170. vobj, err := v.getObject(obj)
  171. if err != nil {
  172. return err
  173. }
  174. f, err := v.Dir.NewFile(obj.Name())
  175. if err != nil {
  176. return err
  177. }
  178. defer f.Close()
  179. return printer.Print(vobj, f)
  180. }
  181. // Directory creates a new temp directory, and allows to easily create new files.
  182. type Directory struct {
  183. Name string
  184. }
  185. // CreateDirectory does create the actual disk directory, and return a
  186. // new representation of it.
  187. func CreateDirectory(prefix string) (*Directory, error) {
  188. name, err := ioutil.TempDir("", prefix+"-")
  189. if err != nil {
  190. return nil, err
  191. }
  192. return &Directory{
  193. Name: name,
  194. }, nil
  195. }
  196. // NewFile creates a new file in the directory.
  197. func (d *Directory) NewFile(name string) (*os.File, error) {
  198. return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)
  199. }
  200. // Delete removes the directory recursively.
  201. func (d *Directory) Delete() error {
  202. return os.RemoveAll(d.Name)
  203. }
  204. // Object is an interface that let's you retrieve multiple version of
  205. // it.
  206. type Object interface {
  207. Live() runtime.Object
  208. Merged() (runtime.Object, error)
  209. Name() string
  210. }
  211. // InfoObject is an implementation of the Object interface. It gets all
  212. // the information from the Info object.
  213. type InfoObject struct {
  214. LocalObj runtime.Object
  215. Info *resource.Info
  216. Encoder runtime.Encoder
  217. OpenAPI openapi.Resources
  218. Force bool
  219. ServerSideApply bool
  220. ForceConflicts bool
  221. }
  222. var _ Object = &InfoObject{}
  223. // Returns the live version of the object
  224. func (obj InfoObject) Live() runtime.Object {
  225. return obj.Info.Object
  226. }
  227. // Returns the "merged" object, as it would look like if applied or
  228. // created.
  229. func (obj InfoObject) Merged() (runtime.Object, error) {
  230. if obj.ServerSideApply {
  231. data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj)
  232. if err != nil {
  233. return nil, err
  234. }
  235. options := metav1.PatchOptions{
  236. Force: &obj.ForceConflicts,
  237. DryRun: []string{metav1.DryRunAll},
  238. }
  239. return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Patch(
  240. obj.Info.Namespace,
  241. obj.Info.Name,
  242. types.ApplyPatchType,
  243. data,
  244. &options,
  245. )
  246. }
  247. // Build the patcher, and then apply the patch with dry-run, unless the object doesn't exist, in which case we need to create it.
  248. if obj.Live() == nil {
  249. // Dry-run create if the object doesn't exist.
  250. return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Create(
  251. obj.Info.Namespace,
  252. true,
  253. obj.LocalObj,
  254. &metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
  255. )
  256. }
  257. var resourceVersion *string
  258. if !obj.Force {
  259. accessor, err := meta.Accessor(obj.Info.Object)
  260. if err != nil {
  261. return nil, err
  262. }
  263. str := accessor.GetResourceVersion()
  264. resourceVersion = &str
  265. }
  266. modified, err := kubectl.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme)
  267. if err != nil {
  268. return nil, err
  269. }
  270. // This is using the patcher from apply, to keep the same behavior.
  271. // We plan on replacing this with server-side apply when it becomes available.
  272. patcher := &apply.Patcher{
  273. Mapping: obj.Info.Mapping,
  274. Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping),
  275. Overwrite: true,
  276. BackOff: clockwork.NewRealClock(),
  277. ServerDryRun: true,
  278. OpenapiSchema: obj.OpenAPI,
  279. ResourceVersion: resourceVersion,
  280. }
  281. _, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil)
  282. return result, err
  283. }
  284. func (obj InfoObject) Name() string {
  285. group := ""
  286. if obj.Info.Mapping.GroupVersionKind.Group != "" {
  287. group = fmt.Sprintf("%v.", obj.Info.Mapping.GroupVersionKind.Group)
  288. }
  289. return group + fmt.Sprintf(
  290. "%v.%v.%v.%v",
  291. obj.Info.Mapping.GroupVersionKind.Version,
  292. obj.Info.Mapping.GroupVersionKind.Kind,
  293. obj.Info.Namespace,
  294. obj.Info.Name,
  295. )
  296. }
  297. // Differ creates two DiffVersion and diffs them.
  298. type Differ struct {
  299. From *DiffVersion
  300. To *DiffVersion
  301. }
  302. func NewDiffer(from, to string) (*Differ, error) {
  303. differ := Differ{}
  304. var err error
  305. differ.From, err = NewDiffVersion(from)
  306. if err != nil {
  307. return nil, err
  308. }
  309. differ.To, err = NewDiffVersion(to)
  310. if err != nil {
  311. differ.From.Dir.Delete()
  312. return nil, err
  313. }
  314. return &differ, nil
  315. }
  316. // Diff diffs to versions of a specific object, and print both versions to directories.
  317. func (d *Differ) Diff(obj Object, printer Printer) error {
  318. if err := d.From.Print(obj, printer); err != nil {
  319. return err
  320. }
  321. if err := d.To.Print(obj, printer); err != nil {
  322. return err
  323. }
  324. return nil
  325. }
  326. // Run runs the diff program against both directories.
  327. func (d *Differ) Run(diff *DiffProgram) error {
  328. return diff.Run(d.From.Dir.Name, d.To.Dir.Name)
  329. }
  330. // TearDown removes both temporary directories recursively.
  331. func (d *Differ) TearDown() {
  332. d.From.Dir.Delete() // Ignore error
  333. d.To.Dir.Delete() // Ignore error
  334. }
  335. func isConflict(err error) bool {
  336. return err != nil && errors.IsConflict(err)
  337. }
  338. func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
  339. var err error
  340. err = o.FilenameOptions.RequireFilenameOrKustomize()
  341. if err != nil {
  342. return err
  343. }
  344. o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd)
  345. o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)
  346. if o.ForceConflicts && !o.ServerSideApply {
  347. return fmt.Errorf("--experimental-force-conflicts only works with --experimental-server-side")
  348. }
  349. if !o.ServerSideApply {
  350. o.OpenAPISchema, err = f.OpenAPISchema()
  351. if err != nil {
  352. return err
  353. }
  354. }
  355. o.DiscoveryClient, err = f.ToDiscoveryClient()
  356. if err != nil {
  357. return err
  358. }
  359. o.DynamicClient, err = f.DynamicClient()
  360. if err != nil {
  361. return err
  362. }
  363. o.DryRunVerifier = &apply.DryRunVerifier{
  364. Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)),
  365. OpenAPIGetter: o.DiscoveryClient,
  366. }
  367. o.CmdNamespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
  368. if err != nil {
  369. return err
  370. }
  371. o.Builder = f.NewBuilder()
  372. return nil
  373. }
  374. // RunDiff uses the factory to parse file arguments, find the version to
  375. // diff, and find each Info object for each files, and runs against the
  376. // differ.
  377. func (o *DiffOptions) Run() error {
  378. differ, err := NewDiffer("LIVE", "MERGED")
  379. if err != nil {
  380. return err
  381. }
  382. defer differ.TearDown()
  383. printer := Printer{}
  384. r := o.Builder.
  385. Unstructured().
  386. NamespaceParam(o.CmdNamespace).DefaultNamespace().
  387. FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
  388. Flatten().
  389. Do()
  390. if err := r.Err(); err != nil {
  391. return err
  392. }
  393. err = r.Visit(func(info *resource.Info, err error) error {
  394. if err != nil {
  395. return err
  396. }
  397. if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
  398. return err
  399. }
  400. local := info.Object.DeepCopyObject()
  401. for i := 1; i <= maxRetries; i++ {
  402. if err = info.Get(); err != nil {
  403. if !errors.IsNotFound(err) {
  404. return err
  405. }
  406. info.Object = nil
  407. }
  408. force := i == maxRetries
  409. if force {
  410. klog.Warningf(
  411. "Object (%v: %v) keeps changing, diffing without lock",
  412. info.Object.GetObjectKind().GroupVersionKind(),
  413. info.Name,
  414. )
  415. }
  416. obj := InfoObject{
  417. LocalObj: local,
  418. Info: info,
  419. Encoder: scheme.DefaultJSONEncoder(),
  420. OpenAPI: o.OpenAPISchema,
  421. Force: force,
  422. ServerSideApply: o.ServerSideApply,
  423. ForceConflicts: o.ForceConflicts,
  424. }
  425. err = differ.Diff(obj, printer)
  426. if !isConflict(err) {
  427. break
  428. }
  429. }
  430. return err
  431. })
  432. if err != nil {
  433. return err
  434. }
  435. return differ.Run(o.Diff)
  436. }