package config import ( "errors" "fmt" "io" "strings" "github.com/evanphx/json-patch" "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/strategicpatch" "k8s.io/kubernetes/pkg/util/yaml" configapi "github.com/openshift/origin/pkg/cmd/server/api" configapiinstall "github.com/openshift/origin/pkg/cmd/server/api/install" "github.com/openshift/origin/pkg/cmd/util/clientcmd" ) const PatchRecommendedName = "patch" var patchTypes = map[string]api.PatchType{"json": api.JSONPatchType, "merge": api.MergePatchType, "strategic": api.StrategicMergePatchType} // PatchOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type PatchOptions struct { Filename string Patch string PatchType api.PatchType Builder *resource.Builder Out io.Writer } const ( patch_long = `Patch the master-config.yaml or node-config.yaml` patch_example = ` # Set the auditConfig.enabled value to true %[1]s openshift.local.config/master/master-config.yaml --patch='{"auditConfig": {"enabled": true}}'` ) func NewCmdPatch(name, fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { o := &PatchOptions{Out: out} cmd := &cobra.Command{ Use: name + " FILENAME -p PATCH", Short: "Update field(s) of a resource using a patch.", Long: patch_long, Example: patch_example, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunPatch()) }, } cmd.Flags().StringVarP(&o.Patch, "patch", "p", "", "The patch to be applied to the resource JSON file.") cmd.MarkFlagRequired("patch") cmd.Flags().String("type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.StringKeySet(patchTypes).List())) return cmd } func (o *PatchOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error { if len(args) != 1 { return fmt.Errorf("exactly one FILENAME is allowed: %v", args) } o.Filename = args[0] patchTypeString := strings.ToLower(cmdutil.GetFlagString(cmd, "type")) ok := false o.PatchType, ok = patchTypes[patchTypeString] if !ok { return cmdutil.UsageError(cmd, fmt.Sprintf("--type must be one of %v, not %q", sets.StringKeySet(patchTypes).List(), patchTypeString)) } o.Builder = resource.NewBuilder(configapiinstall.NewRESTMapper(), configapi.Scheme, resource.DisabledClientForMapping{}, configapi.Codecs.LegacyCodec()) return nil } func (o *PatchOptions) Validate() error { if len(o.Patch) == 0 { return errors.New("must specify -p to patch") } if len(o.Filename) == 0 { return errors.New("filename is required") } return nil } func (o *PatchOptions) RunPatch() error { patchBytes, err := yaml.ToJSON([]byte(o.Patch)) if err != nil { return fmt.Errorf("unable to parse %q: %v", o.Patch, err) } r := o.Builder. FilenameParam(false, false, o.Filename). Flatten(). Do() err = r.Err() if err != nil { return err } infos, err := r.Infos() if err != nil { return err } if len(infos) > 1 { return fmt.Errorf("multiple resources provided") } info := infos[0] originalObjJS, err := runtime.Encode(configapi.Codecs.LegacyCodec(info.Mapping.GroupVersionKind.GroupVersion()), info.VersionedObject.(runtime.Object)) if err != nil { return err } patchedObj, err := configapi.Scheme.DeepCopy(info.VersionedObject) if err != nil { return err } originalPatchedObjJS, err := getPatchedJS(o.PatchType, originalObjJS, patchBytes, patchedObj.(runtime.Object)) if err != nil { return err } rawExtension := &runtime.Unknown{ Raw: originalPatchedObjJS, } printer, _, err := kubectl.GetPrinter("yaml", "") if err != nil { return err } if err := printer.PrintObj(rawExtension, o.Out); err != nil { return err } return nil } func getPatchedJS(patchType api.PatchType, originalJS, patchJS []byte, obj runtime.Object) ([]byte, error) { switch patchType { case api.JSONPatchType: patchObj, err := jsonpatch.DecodePatch(patchJS) if err != nil { return nil, err } return patchObj.Apply(originalJS) case api.MergePatchType: return jsonpatch.MergePatch(originalJS, patchJS) case api.StrategicMergePatchType: return strategicpatch.StrategicMergePatchData(originalJS, patchJS, obj) default: // only here as a safety net - go-restful filters content-type return nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType) } }