GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

save_cmd.go 5.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package main
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "github.com/pkg/errors"
  10. "github.com/spf13/cobra"
  11. "gopkg.in/yaml.v2"
  12. )
  13. type saveOpts struct {
  14. *rootOpts
  15. path string
  16. }
  17. func newSave(parent *rootOpts) *saveOpts {
  18. return &saveOpts{rootOpts: parent}
  19. }
  20. func (opts *saveOpts) Command() *cobra.Command {
  21. cmd := &cobra.Command{
  22. Use: "save --out config/",
  23. Short: "save workload definitions to local files in cluster-native format",
  24. Example: makeExample(
  25. "fluxctl save",
  26. ),
  27. RunE: opts.RunE,
  28. }
  29. cmd.Flags().StringVarP(&opts.path, "out", "o", "-", "Output path for exported config; the default. '-' indicates stdout; if a directory is given, each item will be saved in a file under the directory")
  30. return cmd
  31. }
  32. // Deliberately omit fields (e.g. status, metadata.uid) that we don't want to save
  33. type saveObject struct {
  34. APIVersion string `yaml:"apiVersion,omitempty"`
  35. Kind string `yaml:"kind,omitempty"`
  36. Metadata struct {
  37. Annotations map[string]string `yaml:"annotations,omitempty"`
  38. Labels map[string]string `yaml:"labels,omitempty"`
  39. Name string `yaml:"name,omitempty"`
  40. Namespace string `yaml:"namespace,omitempty"`
  41. } `yaml:"metadata,omitempty"`
  42. Spec map[interface{}]interface{} `yaml:"spec,omitempty"`
  43. }
  44. func (opts *saveOpts) RunE(cmd *cobra.Command, args []string) error {
  45. if len(args) > 0 {
  46. return errorWantedNoArgs
  47. }
  48. ctx := context.Background()
  49. config, err := opts.API.Export(ctx)
  50. if err != nil {
  51. return errors.Wrap(err, "exporting config")
  52. }
  53. if opts.path != "-" {
  54. // check supplied path is a directory
  55. if info, err := os.Stat(opts.path); err != nil {
  56. return err
  57. } else if !info.IsDir() {
  58. return fmt.Errorf("path %s is not a directory", opts.path)
  59. }
  60. }
  61. decoder := yaml.NewDecoder(bytes.NewReader(config))
  62. var decoderErr error
  63. for {
  64. var object saveObject
  65. // Most unwanted fields are ignored at this point
  66. if decoderErr = decoder.Decode(&object); decoderErr != nil {
  67. break
  68. }
  69. // Filter out remaining unwanted keys from unstructured fields
  70. // e.g. .Spec and .Metadata.Annotations
  71. filterObject(object)
  72. if err := saveYAML(cmd.OutOrStdout(), object, opts.path); err != nil {
  73. return errors.Wrap(err, "saving yaml object")
  74. }
  75. }
  76. if decoderErr != io.EOF {
  77. return errors.Wrap(err, "unmarshalling exported yaml")
  78. }
  79. return nil
  80. }
  81. // Remove any data that should not be version controlled
  82. func filterObject(object saveObject) {
  83. delete(object.Metadata.Annotations, "deployment.kubernetes.io/revision")
  84. delete(object.Metadata.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
  85. delete(object.Metadata.Annotations, "kubernetes.io/change-cause")
  86. deleteNested(object.Spec, "template", "metadata", "creationTimestamp")
  87. deleteEmptyMapValues(object.Spec)
  88. }
  89. // Recurse through nested maps to remove a key
  90. func deleteNested(m map[interface{}]interface{}, keys ...string) {
  91. switch len(keys) {
  92. case 0:
  93. return
  94. case 1:
  95. delete(m, keys[0])
  96. default:
  97. if v, ok := m[keys[0]].(map[interface{}]interface{}); ok {
  98. deleteNested(v, keys[1:]...)
  99. }
  100. }
  101. }
  102. // Recursively delete map keys with empty values
  103. func deleteEmptyMapValues(i interface{}) bool {
  104. switch i := i.(type) {
  105. case map[interface{}]interface{}:
  106. if len(i) == 0 {
  107. return true
  108. } else {
  109. for k, v := range i {
  110. if deleteEmptyMapValues(v) {
  111. delete(i, k)
  112. }
  113. }
  114. }
  115. case []interface{}:
  116. if len(i) == 0 {
  117. return true
  118. } else {
  119. for _, e := range i {
  120. deleteEmptyMapValues(e)
  121. }
  122. }
  123. case nil:
  124. return true
  125. }
  126. return false
  127. }
  128. func outputFile(stdout io.Writer, object saveObject, out string) (string, error) {
  129. var path string
  130. if object.Kind == "Namespace" {
  131. path = fmt.Sprintf("%s-ns.yaml", object.Metadata.Name)
  132. } else {
  133. dir := object.Metadata.Namespace
  134. if err := os.MkdirAll(filepath.Join(out, dir), os.ModePerm); err != nil {
  135. return "", errors.Wrap(err, "making directory for namespace")
  136. }
  137. shortKind := abbreviateKind(object.Kind)
  138. path = filepath.Join(dir, fmt.Sprintf("%s-%s.yaml", object.Metadata.Name, shortKind))
  139. }
  140. path = filepath.Join(out, path)
  141. fmt.Fprintf(stdout, "Saving %s '%s' to %s\n", object.Kind, object.Metadata.Name, path)
  142. return path, nil
  143. }
  144. // Save YAML to directory structure
  145. func saveYAML(stdout io.Writer, object saveObject, out string) error {
  146. buf, err := yaml.Marshal(object)
  147. if err != nil {
  148. return errors.Wrap(err, "marshalling yaml")
  149. }
  150. // to stdout
  151. if out == "-" {
  152. fmt.Fprintln(stdout, "---")
  153. fmt.Fprint(stdout, string(buf))
  154. return nil
  155. }
  156. // to a directory
  157. path, err := outputFile(stdout, object, out)
  158. if err != nil {
  159. return err
  160. }
  161. file, err := os.Create(path)
  162. if err != nil {
  163. return errors.Wrap(err, "creating yaml file")
  164. }
  165. defer file.Close()
  166. // We prepend a document separator, because it helps when files
  167. // are cat'd together, and is otherwise harmless.
  168. fmt.Fprintln(file, "---")
  169. if _, err := file.Write(buf); err != nil {
  170. return errors.Wrap(err, "writing yaml file")
  171. }
  172. return nil
  173. }
  174. func abbreviateKind(kind string) string {
  175. switch kind {
  176. case "Deployment":
  177. return "dep"
  178. default:
  179. return kind
  180. }
  181. }