diff --git a/.generated_docs b/.generated_docs index fba53f0a03e..b61bb953990 100644 --- a/.generated_docs +++ b/.generated_docs @@ -15,6 +15,7 @@ docs/man/man1/kubectl-config.1 docs/man/man1/kubectl-create.1 docs/man/man1/kubectl-delete.1 docs/man/man1/kubectl-describe.1 +docs/man/man1/kubectl-edit.1 docs/man/man1/kubectl-exec.1 docs/man/man1/kubectl-expose.1 docs/man/man1/kubectl-get.1 @@ -47,6 +48,7 @@ docs/user-guide/kubectl/kubectl_config_view.md docs/user-guide/kubectl/kubectl_create.md docs/user-guide/kubectl/kubectl_delete.md docs/user-guide/kubectl/kubectl_describe.md +docs/user-guide/kubectl/kubectl_edit.md docs/user-guide/kubectl/kubectl_exec.md docs/user-guide/kubectl/kubectl_expose.md docs/user-guide/kubectl/kubectl_get.md diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 5295dc6b33b..a25685db766 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -479,6 +479,30 @@ _kubectl_delete() must_have_one_noun+=("thirdpartyresource") } +_kubectl_edit() +{ + last_command="kubectl_edit" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--filename=") + flags_with_completion+=("--filename") + flags_completion+=("__handle_filename_extension_flag json|stdin|yaml|yml") + two_word_flags+=("-f") + flags_with_completion+=("-f") + flags_completion+=("__handle_filename_extension_flag json|stdin|yaml|yml") + flags+=("--output=") + two_word_flags+=("-o") + flags+=("--output-version=") + + must_have_one_flag=() + must_have_one_noun=() +} + _kubectl_namespace() { last_command="kubectl_namespace" @@ -1087,6 +1111,7 @@ _kubectl() commands+=("replace") commands+=("patch") commands+=("delete") + commands+=("edit") commands+=("namespace") commands+=("logs") commands+=("rolling-update") diff --git a/docs/man/man1/.files_generated b/docs/man/man1/.files_generated new file mode 100644 index 00000000000..078d014bcf5 --- /dev/null +++ b/docs/man/man1/.files_generated @@ -0,0 +1,32 @@ +kubectl-annotate.1 +kubectl-api-versions.1 +kubectl-attach.1 +kubectl-cluster-info.1 +kubectl-config-set-cluster.1 +kubectl-config-set-context.1 +kubectl-config-set-credentials.1 +kubectl-config-set.1 +kubectl-config-unset.1 +kubectl-config-use-context.1 +kubectl-config-view.1 +kubectl-config.1 +kubectl-create.1 +kubectl-delete.1 +kubectl-describe.1 +kubectl-edit.1 +kubectl-exec.1 +kubectl-expose.1 +kubectl-get.1 +kubectl-label.1 +kubectl-logs.1 +kubectl-namespace.1 +kubectl-patch.1 +kubectl-port-forward.1 +kubectl-proxy.1 +kubectl-replace.1 +kubectl-rolling-update.1 +kubectl-run.1 +kubectl-scale.1 +kubectl-stop.1 +kubectl-version.1 +kubectl.1 diff --git a/docs/man/man1/kubectl-edit.1 b/docs/man/man1/kubectl-edit.1 new file mode 100644 index 00000000000..0f6f8377dd2 --- /dev/null +++ b/docs/man/man1/kubectl-edit.1 @@ -0,0 +1,172 @@ +.TH "KUBERNETES" "1" " kubernetes User Manuals" "Eric Paris" "Jan 2015" "" + + +.SH NAME +.PP +kubectl edit \- Edit a resource on the server + + +.SH SYNOPSIS +.PP +\fBkubectl edit\fP [OPTIONS] + + +.SH DESCRIPTION +.PP +Edit a resource from the default editor. + +.PP +The edit command allows you to directly edit any API resource you can retrieve via the +command line tools. It will open the editor defined by your KUBE\_EDITOR, GIT\_EDITOR, +or EDITOR environment variables, or fall back to 'vi'. You can edit multiple objects, +although changes are applied one at a time. The command accepts filenames as well as +command line arguments, although the files you point to must be previously saved +versions of resources. + +.PP +The files to edit will be output in the default API version, or a version specified +by \-\-output\-version. The default format is YAML \- if you would like to edit in JSON +pass \-o json. + +.PP +In the event an error occurs while updating, a temporary file will be created on disk +that contains your unapplied changes. The most common error when updating a resource +is another editor changing the resource on the server. When this occurs, you will have +to apply your changes to the newer version of the resource, or update your temporary +saved copy to include the latest resource version. + + +.SH OPTIONS +.PP +\fB\-f\fP, \fB\-\-filename\fP=[] + Filename, directory, or URL to file to use to edit the resource + +.PP +\fB\-o\fP, \fB\-\-output\fP="yaml" + Output format. One of: yaml|json. + +.PP +\fB\-\-output\-version\fP="" + Output the formatted object with the given version (default api\-version). + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-\-alsologtostderr\fP=false + log to standard error as well as files + +.PP +\fB\-\-api\-version\fP="" + The API version to use when talking to the server + +.PP +\fB\-\-certificate\-authority\fP="" + Path to a cert. file for the certificate authority. + +.PP +\fB\-\-client\-certificate\fP="" + Path to a client key file for TLS. + +.PP +\fB\-\-client\-key\fP="" + Path to a client key file for TLS. + +.PP +\fB\-\-cluster\fP="" + The name of the kubeconfig cluster to use + +.PP +\fB\-\-context\fP="" + The name of the kubeconfig context to use + +.PP +\fB\-\-insecure\-skip\-tls\-verify\fP=false + If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure. + +.PP +\fB\-\-kubeconfig\fP="" + Path to the kubeconfig file to use for CLI requests. + +.PP +\fB\-\-log\-backtrace\-at\fP=:0 + when logging hits line file:N, emit a stack trace + +.PP +\fB\-\-log\-dir\fP="" + If non\-empty, write log files in this directory + +.PP +\fB\-\-log\-flush\-frequency\fP=5s + Maximum number of seconds between log flushes + +.PP +\fB\-\-logtostderr\fP=true + log to standard error instead of files + +.PP +\fB\-\-match\-server\-version\fP=false + Require server version to match client version + +.PP +\fB\-\-namespace\fP="" + If present, the namespace scope for this CLI request. + +.PP +\fB\-\-password\fP="" + Password for basic authentication to the API server. + +.PP +\fB\-s\fP, \fB\-\-server\fP="" + The address and port of the Kubernetes API server + +.PP +\fB\-\-stderrthreshold\fP=2 + logs at or above this threshold go to stderr + +.PP +\fB\-\-token\fP="" + Bearer token for authentication to the API server. + +.PP +\fB\-\-user\fP="" + The name of the kubeconfig user to use + +.PP +\fB\-\-username\fP="" + Username for basic authentication to the API server. + +.PP +\fB\-\-v\fP=0 + log level for V logs + +.PP +\fB\-\-vmodule\fP= + comma\-separated list of pattern=N settings for file\-filtered logging + + +.SH EXAMPLE +.PP +.RS + +.nf + # Edit the service named 'docker\-registry': + $ kubectl edit svc/docker\-registry + + # Use an alternative editor + $ KUBE\_EDITOR="nano" kubectl edit svc/docker\-registry + + # Edit the service 'docker\-registry' in JSON using the v1 API format: + $ kubectl edit svc/docker\-registry \-\-output\-version=v1 \-o json + +.fi +.RE + + +.SH SEE ALSO +.PP +\fBkubectl(1)\fP, + + +.SH HISTORY +.PP +January 2015, Originally compiled by Eric Paris (eparis at redhat dot com) based on the kubernetes source material, but hopefully they have been automatically generated since! diff --git a/docs/man/man1/kubectl.1 b/docs/man/man1/kubectl.1 index a95623c3aca..357b51a6601 100644 --- a/docs/man/man1/kubectl.1 +++ b/docs/man/man1/kubectl.1 @@ -116,7 +116,7 @@ Find more information at .SH SEE ALSO .PP -\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-annotate(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP, +\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-edit(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-annotate(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP, .SH HISTORY diff --git a/docs/user-guide/kubectl/.files_generated b/docs/user-guide/kubectl/.files_generated new file mode 100644 index 00000000000..52fd90016a5 --- /dev/null +++ b/docs/user-guide/kubectl/.files_generated @@ -0,0 +1,32 @@ +kubectl.md +kubectl_annotate.md +kubectl_api-versions.md +kubectl_attach.md +kubectl_cluster-info.md +kubectl_config.md +kubectl_config_set-cluster.md +kubectl_config_set-context.md +kubectl_config_set-credentials.md +kubectl_config_set.md +kubectl_config_unset.md +kubectl_config_use-context.md +kubectl_config_view.md +kubectl_create.md +kubectl_delete.md +kubectl_describe.md +kubectl_edit.md +kubectl_exec.md +kubectl_expose.md +kubectl_get.md +kubectl_label.md +kubectl_logs.md +kubectl_namespace.md +kubectl_patch.md +kubectl_port-forward.md +kubectl_proxy.md +kubectl_replace.md +kubectl_rolling-update.md +kubectl_run.md +kubectl_scale.md +kubectl_stop.md +kubectl_version.md diff --git a/docs/user-guide/kubectl/kubectl.md b/docs/user-guide/kubectl/kubectl.md index 32f2e3eca1d..58631bbb75b 100644 --- a/docs/user-guide/kubectl/kubectl.md +++ b/docs/user-guide/kubectl/kubectl.md @@ -84,6 +84,7 @@ kubectl * [kubectl create](kubectl_create.md) - Create a resource by filename or stdin * [kubectl delete](kubectl_delete.md) - Delete resources by filenames, stdin, resources and names, or by resources and label selector. * [kubectl describe](kubectl_describe.md) - Show details of a specific resource or group of resources +* [kubectl edit](kubectl_edit.md) - Edit a resource on the server * [kubectl exec](kubectl_exec.md) - Execute a command in a container. * [kubectl expose](kubectl_expose.md) - Take a replication controller, service or pod and expose it as a new Kubernetes Service * [kubectl get](kubectl_get.md) - Display one or many resources diff --git a/docs/user-guide/kubectl/kubectl_edit.md b/docs/user-guide/kubectl/kubectl_edit.md new file mode 100644 index 00000000000..04ef1749998 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_edit.md @@ -0,0 +1,121 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +If you are using a released version of Kubernetes, you should +refer to the docs that go with that version. + + +The latest 1.0.x release of this document can be found +[here](http://releases.k8s.io/release-1.0/docs/user-guide/kubectl/kubectl_edit.md). + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +## kubectl edit + +Edit a resource on the server + +### Synopsis + + +Edit a resource from the default editor. + +The edit command allows you to directly edit any API resource you can retrieve via the +command line tools. It will open the editor defined by your KUBE_EDITOR, GIT_EDITOR, +or EDITOR environment variables, or fall back to 'vi'. You can edit multiple objects, +although changes are applied one at a time. The command accepts filenames as well as +command line arguments, although the files you point to must be previously saved +versions of resources. + +The files to edit will be output in the default API version, or a version specified +by --output-version. The default format is YAML - if you would like to edit in JSON +pass -o json. + +In the event an error occurs while updating, a temporary file will be created on disk +that contains your unapplied changes. The most common error when updating a resource +is another editor changing the resource on the server. When this occurs, you will have +to apply your changes to the newer version of the resource, or update your temporary +saved copy to include the latest resource version. + +``` +kubectl edit (RESOURCE/NAME | -f FILENAME) +``` + +### Examples + +``` + # Edit the service named 'docker-registry': + $ kubectl edit svc/docker-registry + + # Use an alternative editor + $ KUBE_EDITOR="nano" kubectl edit svc/docker-registry + + # Edit the service 'docker-registry' in JSON using the v1 API format: + $ kubectl edit svc/docker-registry --output-version=v1 -o json +``` + +### Options + +``` + -f, --filename=[]: Filename, directory, or URL to file to use to edit the resource + -o, --output="yaml": Output format. One of: yaml|json. + --output-version="": Output the formatted object with the given version (default api-version). +``` + +### Options inherited from parent commands + +``` + --alsologtostderr[=false]: log to standard error as well as files + --api-version="": The API version to use when talking to the server + --certificate-authority="": Path to a cert. file for the certificate authority. + --client-certificate="": Path to a client key file for TLS. + --client-key="": Path to a client key file for TLS. + --cluster="": The name of the kubeconfig cluster to use + --context="": The name of the kubeconfig context to use + --insecure-skip-tls-verify[=false]: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure. + --kubeconfig="": Path to the kubeconfig file to use for CLI requests. + --log-backtrace-at=:0: when logging hits line file:N, emit a stack trace + --log-dir="": If non-empty, write log files in this directory + --log-flush-frequency=5s: Maximum number of seconds between log flushes + --logtostderr[=true]: log to standard error instead of files + --match-server-version[=false]: Require server version to match client version + --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. + -s, --server="": The address and port of the Kubernetes API server + --stderrthreshold=2: logs at or above this threshold go to stderr + --token="": Bearer token for authentication to the API server. + --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. + --v=0: log level for V logs + --vmodule=: comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager + +###### Auto generated by spf13/cobra at 2015-09-16 00:43:02.024642139 +0000 UTC + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_edit.md?pixel)]() + diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 0c85a56d36b..9c53185152b 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -397,11 +397,21 @@ runTests() { ## --force replace pod can change other field, e.g., spec.container.name # Command - kubectl get "${kube_flags[@]}" pod valid-pod -o json | sed 's/"kubernetes-serve-hostname"/"replaced-k8s-serve-hostname"/g' > tmp-valid-pod.json - kubectl replace "${kube_flags[@]}" --force -f tmp-valid-pod.json + kubectl get "${kube_flags[@]}" pod valid-pod -o json | sed 's/"kubernetes-serve-hostname"/"replaced-k8s-serve-hostname"/g' > /tmp/tmp-valid-pod.json + kubectl replace "${kube_flags[@]}" --force -f /tmp/tmp-valid-pod.json # Post-condition: spec.container.name = "replaced-k8s-serve-hostname" kube::test::get_object_assert 'pod valid-pod' "{{(index .spec.containers 0).name}}" 'replaced-k8s-serve-hostname' - rm tmp-valid-pod.json + #cleaning + rm /tmp/tmp-valid-pod.json + + ## kubectl edit can update the image field of a POD. tmp-editor.sh is a fake editor + echo -e '#!/bin/bash\nsed -i "s/kubernetes\/pause/gcr.io\/google_containers\/serve_hostname/g" $1' > /tmp/tmp-editor.sh + chmod +x /tmp/tmp-editor.sh + EDITOR=/tmp/tmp-editor.sh kubectl edit "${kube_flags[@]}" pods/valid-pod + # Post-condition: valid-pod POD has image gcr.io/google_containers/serve_hostname + kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'gcr.io/google_containers/serve_hostname:' + # cleaning + rm /tmp/tmp-editor.sh ### Overwriting an existing label is not permitted # Pre-condition: name is valid-pod diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 3e40a225820..af6aa742ddc 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -149,6 +149,7 @@ Find more information at https://github.com/kubernetes/kubernetes.`, cmds.AddCommand(NewCmdReplace(f, out)) cmds.AddCommand(NewCmdPatch(f, out)) cmds.AddCommand(NewCmdDelete(f, out)) + cmds.AddCommand(NewCmdEdit(f, out)) cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(NewCmdLog(f, out)) diff --git a/pkg/kubectl/cmd/edit.go b/pkg/kubectl/cmd/edit.go new file mode 100644 index 00000000000..f056563f856 --- /dev/null +++ b/pkg/kubectl/cmd/edit.go @@ -0,0 +1,392 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/kubectl" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/cmd/util/editor" + "k8s.io/kubernetes/pkg/kubectl/cmd/util/jsonmerge" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/util/strategicpatch" + "k8s.io/kubernetes/pkg/util/yaml" + + "github.com/golang/glog" + "github.com/spf13/cobra" +) + +const ( + editLong = `Edit a resource from the default editor. + +The edit command allows you to directly edit any API resource you can retrieve via the +command line tools. It will open the editor defined by your KUBE_EDITOR, GIT_EDITOR, +or EDITOR environment variables, or fall back to 'vi'. You can edit multiple objects, +although changes are applied one at a time. The command accepts filenames as well as +command line arguments, although the files you point to must be previously saved +versions of resources. + +The files to edit will be output in the default API version, or a version specified +by --output-version. The default format is YAML - if you would like to edit in JSON +pass -o json. + +In the event an error occurs while updating, a temporary file will be created on disk +that contains your unapplied changes. The most common error when updating a resource +is another editor changing the resource on the server. When this occurs, you will have +to apply your changes to the newer version of the resource, or update your temporary +saved copy to include the latest resource version.` + + editExample = ` # Edit the service named 'docker-registry': + $ kubectl edit svc/docker-registry + + # Use an alternative editor + $ KUBE_EDITOR="nano" kubectl edit svc/docker-registry + + # Edit the service 'docker-registry' in JSON using the v1 API format: + $ kubectl edit svc/docker-registry --output-version=v1 -o json` +) + +var errExit = fmt.Errorf("exit directly") + +func NewCmdEdit(f *cmdutil.Factory, out io.Writer) *cobra.Command { + filenames := []string{} + cmd := &cobra.Command{ + Use: "edit (RESOURCE/NAME | -f FILENAME)", + Short: "Edit a resource on the server", + Long: editLong, + Example: fmt.Sprintf(editExample), + Run: func(cmd *cobra.Command, args []string) { + err := RunEdit(f, out, cmd, args, filenames) + if err == errExit { + os.Exit(1) + } + cmdutil.CheckErr(err) + }, + } + usage := "Filename, directory, or URL to file to use to edit the resource" + kubectl.AddJsonFilenameFlag(cmd, &filenames, usage) + cmd.Flags().StringP("output", "o", "yaml", "Output format. One of: yaml|json.") + cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).") + return cmd +} + +func RunEdit(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, filenames []string) error { + var printer kubectl.ResourcePrinter + var ext string + switch format := cmdutil.GetFlagString(cmd, "output"); format { + case "json": + printer = &kubectl.JSONPrinter{} + ext = ".json" + case "yaml": + printer = &kubectl.YAMLPrinter{} + ext = ".yaml" + default: + return cmdutil.UsageError(cmd, "The flag 'output' must be one of yaml|json") + } + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + mapper, typer := f.Object() + rmap := &resource.Mapper{ + ObjectTyper: typer, + RESTMapper: mapper, + ClientMapper: f.ClientMapperForCommand(), + } + + r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, filenames...). + ResourceTypeOrNameArgs(true, args...). + Latest(). + Flatten(). + Do() + err = r.Err() + if err != nil { + return err + } + + infos, err := r.Infos() + if err != nil { + return err + } + + clientConfig, err := f.ClientConfig() + if err != nil { + return err + } + + defaultVersion := cmdutil.OutputVersion(cmd, clientConfig.Version) + results := editResults{} + for { + obj, err := resource.AsVersionedObject(infos, false, defaultVersion) + if err != nil { + return preservedFile(err, results.file, out) + } + + // TODO: add an annotating YAML printer that can print inline comments on each field, + // including descriptions or validation errors + + // generate the file to edit + buf := &bytes.Buffer{} + if err := results.header.writeTo(buf); err != nil { + return preservedFile(err, results.file, out) + } + if err := printer.PrintObj(obj, buf); err != nil { + return preservedFile(err, results.file, out) + } + original := buf.Bytes() + + // launch the editor + edit := editor.NewDefaultEditor() + edited, file, err := edit.LaunchTempFile("kubectl-edit-", ext, buf) + if err != nil { + return preservedFile(err, results.file, out) + } + + // cleanup any file from the previous pass + if len(results.file) > 0 { + os.Remove(results.file) + } + + glog.V(4).Infof("User edited:\n%s", string(edited)) + fmt.Printf("User edited:\n%s", string(edited)) + lines, err := hasLines(bytes.NewBuffer(edited)) + if err != nil { + return preservedFile(err, file, out) + } + if bytes.Equal(original, edited) { + if len(results.edit) > 0 { + preservedFile(nil, file, out) + } else { + os.Remove(file) + } + fmt.Fprintln(out, "Edit cancelled, no changes made.") + return nil + } + if !lines { + if len(results.edit) > 0 { + preservedFile(nil, file, out) + } else { + os.Remove(file) + } + fmt.Fprintln(out, "Edit cancelled, saved file was empty.") + return nil + } + + results = editResults{ + file: file, + } + + // parse the edited file + updates, err := rmap.InfoForData(edited, "edited-file") + if err != nil { + return preservedFile(err, file, out) + } + + visitor := resource.NewFlattenListVisitor(updates, rmap) + + // need to make sure the original namespace wasn't changed while editing + if err = visitor.Visit(resource.RequireNamespace(cmdNamespace)); err != nil { + return preservedFile(err, file, out) + } + + // use strategic merge to create a patch + originalJS, err := yaml.ToJSON(original) + if err != nil { + return preservedFile(err, file, out) + } + editedJS, err := yaml.ToJSON(edited) + if err != nil { + return preservedFile(err, file, out) + } + patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, obj) + // TODO: change all jsonmerge to strategicpatch + // for checking preconditions + preconditions := []jsonmerge.PreconditionFunc{} + if err != nil { + glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + return preservedFile(err, file, out) + } else { + preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("apiVersion")) + preconditions = append(preconditions, jsonmerge.RequireKeyUnchanged("kind")) + preconditions = append(preconditions, jsonmerge.RequireMetadataKeyUnchanged("name")) + results.version = defaultVersion + } + + if hold, msg := jsonmerge.TestPreconditionsHold(patch, preconditions); !hold { + fmt.Fprintf(out, "error: %s", msg) + return preservedFile(nil, file, out) + } + + err = visitor.Visit(func(info *resource.Info, err error) error { + patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch) + if err != nil { + fmt.Fprintln(out, results.addError(err, info)) + return nil + } + info.Refresh(patched, true) + cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "edited") + return nil + }) + if err != nil { + return preservedFile(err, file, out) + } + + if results.retryable > 0 { + fmt.Fprintf(out, "You can run `kubectl replace -f %s` to try this update again.\n", file) + return errExit + } + if results.conflict > 0 { + fmt.Fprintf(out, "You must update your local resource version and run `kubectl replace -f %s` to overwrite the remote changes.\n", file) + return errExit + } + if len(results.edit) == 0 { + if results.notfound == 0 { + os.Remove(file) + } else { + fmt.Fprintf(out, "The edits you made on deleted resources have been saved to %q\n", file) + } + return nil + } + + // loop again and edit the remaining items + infos = results.edit + } + return nil +} + +// print json file (such as patch file) content for debugging +func printJson(out io.Writer, file []byte) error { + diff := make(map[string]interface{}) + if err := json.Unmarshal(file, &diff); err != nil { + return err + } + fmt.Fprintf(out, "%v\n", diff) + return nil +} + +// editReason preserves a message about the reason this file must be edited again +type editReason struct { + head string + other []string +} + +// editHeader includes a list of reasons the edit must be retried +type editHeader struct { + reasons []editReason +} + +// writeTo outputs the current header information into a stream +func (h *editHeader) writeTo(w io.Writer) error { + fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, +# and an empty file will abort the edit. If an error occurs while saving this file will be +# reopened with the relevant failures. +# +`) + for _, r := range h.reasons { + if len(r.other) > 0 { + fmt.Fprintf(w, "# %s:\n", r.head) + } else { + fmt.Fprintf(w, "# %s\n", r.head) + } + for _, o := range r.other { + fmt.Fprintf(w, "# * %s\n", o) + } + fmt.Fprintln(w, "#") + } + return nil +} + +// editResults capture the result of an update +type editResults struct { + header editHeader + retryable int + notfound int + conflict int + edit []*resource.Info + file string + + version string +} + +func (r *editResults) addError(err error, info *resource.Info) string { + switch { + case errors.IsInvalid(err): + r.edit = append(r.edit, info) + reason := editReason{ + head: fmt.Sprintf("%s %s was not valid", info.Mapping.Kind, info.Name), + } + if err, ok := err.(client.APIStatus); ok { + if details := err.Status().Details; details != nil { + for _, cause := range details.Causes { + reason.other = append(reason.other, cause.Message) + } + } + } + r.header.reasons = append(r.header.reasons, reason) + return fmt.Sprintf("Error: the %s %s is invalid", info.Mapping.Kind, info.Name) + case errors.IsNotFound(err): + r.notfound++ + return fmt.Sprintf("Error: the %s %s could not be found on the server", info.Mapping.Kind, info.Name) + default: + r.retryable++ + return fmt.Sprintf("Error: the %s %s could not be patched: %v", info.Mapping.Kind, info.Name, err) + } +} + +// preservedFile writes out a message about the provided file if it exists to the +// provided output stream when an error happens. Used to notify the user where +// their updates were preserved. +func preservedFile(err error, path string, out io.Writer) error { + if len(path) > 0 { + if _, err := os.Stat(path); !os.IsNotExist(err) { + fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path) + } + } + return err +} + +// hasLines returns true if any line in the provided stream is non empty - has non-whitespace +// characters, or the first non-whitespace character is a '#' indicating a comment. Returns +// any errors encountered reading the stream. +func hasLines(r io.Reader) (bool, error) { + // TODO: if any files we read have > 64KB lines, we'll need to switch to bytes.ReadLine + // TODO: probably going to be secrets + s := bufio.NewScanner(r) + for s.Scan() { + if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' { + return true, nil + } + } + if err := s.Err(); err != nil && err != io.EOF { + return false, err + } + return false, nil +} diff --git a/pkg/kubectl/cmd/util/editor/editor.go b/pkg/kubectl/cmd/util/editor/editor.go new file mode 100644 index 00000000000..2145792e901 --- /dev/null +++ b/pkg/kubectl/cmd/util/editor/editor.go @@ -0,0 +1,199 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package editor + +import ( + "fmt" + "io" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/term" + "github.com/golang/glog" +) + +const ( + // sorry, blame Git + defaultEditor = "vi" + defaultShell = "/bin/bash" +) + +type Editor struct { + Args []string + Shell bool +} + +// NewDefaultEditor creates a struct Editor that uses the OS environment to +// locate the editor program, looking at EDITOR environment variable to find +// the proper command line. If the provided editor has no spaces, or no quotes, +// it is treated as a bare command to be loaded. Otherwise, the string will +// be passed to the user's shell for execution. +func NewDefaultEditor() Editor { + args, shell := defaultEnvEditor() + return Editor{ + Args: args, + Shell: shell, + } +} + +func defaultEnvShell() []string { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = defaultShell + } + return []string{shell, "-c"} +} + +func defaultEnvEditor() ([]string, bool) { + editor := os.Getenv("EDITOR") + if len(editor) == 0 { + editor = defaultEditor + } + if !strings.Contains(editor, " ") { + return []string{editor}, false + } + if !strings.ContainsAny(editor, "\"'\\") { + return strings.Split(editor, " "), false + } + // rather than parse the shell arguments ourselves, punt to the shell + shell := defaultEnvShell() + return append(shell, editor), true +} + +func (e Editor) args(path string) []string { + args := make([]string, len(e.Args)) + copy(args, e.Args) + if e.Shell { + last := args[len(args)-1] + args[len(args)-1] = fmt.Sprintf("%s %q", last, path) + } else { + args = append(args, path) + } + return args +} + +// Launch opens the described or returns an error. The TTY will be protected, and +// SIGQUIT, SIGTERM, and SIGINT will all be trapped. +func (e Editor) Launch(path string) error { + if len(e.Args) == 0 { + return fmt.Errorf("no editor defined, can't open %s", path) + } + abs, err := filepath.Abs(path) + if err != nil { + return err + } + args := e.args(abs) + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + glog.V(5).Infof("Opening file with editor %v", args) + if err := withSafeTTYAndInterrupts(cmd.Run); err != nil { + if err, ok := err.(*exec.Error); ok { + if err.Err == exec.ErrNotFound { + return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " ")) + } + } + return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " ")) + } + return nil +} + +// LaunchTempFile reads the provided stream into a temporary file in the given directory +// and file prefix, and then invokes Launch with the path of that file. It will return +// the contents of the file after launch, any errors that occur, and the path of the +// temporary file so the caller can clean it up as needed. +func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) { + f, err := tempFile(prefix, suffix) + if err != nil { + return nil, "", err + } + defer f.Close() + path := f.Name() + if _, err := io.Copy(f, r); err != nil { + os.Remove(path) + return nil, path, err + } + if err := e.Launch(path); err != nil { + return nil, path, err + } + bytes, err := ioutil.ReadFile(path) + return bytes, path, err +} + +// withSafeTTYAndInterrupts invokes the provided function after the terminal +// state has been stored, and then on any error or termination attempts to +// restore the terminal state to its prior behavior. It also eats signals +// for the duration of the function. +func withSafeTTYAndInterrupts(fn func() error) error { + ch := make(chan os.Signal, 1) + signal.Notify(ch, childSignals...) + defer signal.Stop(ch) + + inFd := os.Stdin.Fd() + if !term.IsTerminal(inFd) { + if f, err := os.Open("/dev/tty"); err == nil { + defer f.Close() + inFd = f.Fd() + } + } + + if term.IsTerminal(inFd) { + state, err := term.SaveState(inFd) + if err != nil { + return err + } + go func() { + if _, ok := <-ch; !ok { + return + } + term.RestoreTerminal(inFd, state) + }() + defer term.RestoreTerminal(inFd, state) + return fn() + } + return fn() +} + +func tempFile(prefix, suffix string) (f *os.File, err error) { + dir := os.TempDir() + + for i := 0; i < 10000; i++ { + name := filepath.Join(dir, prefix+randSeq(5)+suffix) + f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + continue + } + break + } + return +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} diff --git a/pkg/kubectl/cmd/util/editor/editor_test.go b/pkg/kubectl/cmd/util/editor/editor_test.go new file mode 100644 index 00000000000..9be83a04276 --- /dev/null +++ b/pkg/kubectl/cmd/util/editor/editor_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package editor + +import ( + "bytes" + "io/ioutil" + "os" + "reflect" + "strings" + "testing" +) + +func TestArgs(t *testing.T) { + if e, a := []string{"/bin/bash", "-c \"test\""}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { + t.Errorf("unexpected args: %v", a) + } + if e, a := []string{"/bin/bash", "-c", "test"}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: false}).args("test"); !reflect.DeepEqual(e, a) { + t.Errorf("unexpected args: %v", a) + } + if e, a := []string{"/bin/bash", "-i -c \"test\""}, (Editor{Args: []string{"/bin/bash", "-i -c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { + t.Errorf("unexpected args: %v", a) + } + if e, a := []string{"/test", "test"}, (Editor{Args: []string{"/test"}}).args("test"); !reflect.DeepEqual(e, a) { + t.Errorf("unexpected args: %v", a) + } +} + +func TestEditor(t *testing.T) { + edit := Editor{Args: []string{"cat"}} + testStr := "test something\n" + contents, path, err := edit.LaunchTempFile("", "someprefix", bytes.NewBufferString(testStr)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, err := os.Stat(path); err != nil { + t.Fatalf("no temp file: %s", path) + } + defer os.Remove(path) + if disk, err := ioutil.ReadFile(path); err != nil || !bytes.Equal(contents, disk) { + t.Errorf("unexpected file on disk: %v %s", err, string(disk)) + } + if !bytes.Equal(contents, []byte(testStr)) { + t.Errorf("unexpected contents: %s", string(contents)) + } + if !strings.Contains(path, "someprefix") { + t.Errorf("path not expected: %s", path) + } +} diff --git a/pkg/kubectl/cmd/util/editor/term.go b/pkg/kubectl/cmd/util/editor/term.go new file mode 100644 index 00000000000..9db85fe4b06 --- /dev/null +++ b/pkg/kubectl/cmd/util/editor/term.go @@ -0,0 +1,27 @@ +// +build !windows + +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package editor + +import ( + "os" + "syscall" +) + +// childSignals are the allowed signals that can be sent to children in Unix variant OS's +var childSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT} diff --git a/pkg/kubectl/cmd/util/editor/term_unsupported.go b/pkg/kubectl/cmd/util/editor/term_unsupported.go new file mode 100644 index 00000000000..4c0b788166d --- /dev/null +++ b/pkg/kubectl/cmd/util/editor/term_unsupported.go @@ -0,0 +1,26 @@ +// +build windows + +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package editor + +import ( + "os" +) + +// childSignals are the allowed signals that can be sent to children in Windows to terminate +var childSignals = []os.Signal{os.Interrupt} diff --git a/pkg/kubectl/cmd/util/jsonmerge/jsonmerge.go b/pkg/kubectl/cmd/util/jsonmerge/jsonmerge.go new file mode 100644 index 00000000000..c3b2244dbfb --- /dev/null +++ b/pkg/kubectl/cmd/util/jsonmerge/jsonmerge.go @@ -0,0 +1,227 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonmerge + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/evanphx/json-patch" + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/util/yaml" +) + +// Delta represents a change between two JSON documents. +type Delta struct { + original []byte + edit []byte + + preconditions []PreconditionFunc +} + +// PreconditionFunc is a test to verify that an incompatible change +// has occurred before an Apply can be successful. +type PreconditionFunc func(interface{}) (hold bool, message string) + +// AddPreconditions adds precondition checks to a change which must +// be satisfied before an Apply is considered successful. If a +// precondition returns false, the Apply is failed with +// ErrPreconditionFailed. +func (d *Delta) AddPreconditions(fns ...PreconditionFunc) { + d.preconditions = append(d.preconditions, fns...) +} + +// RequireKeyUnchanged creates a precondition function that fails +// if the provided key is present in the diff (indicating its value +// has changed). +func RequireKeyUnchanged(key string) PreconditionFunc { + return func(diff interface{}) (bool, string) { + m, ok := diff.(map[string]interface{}) + if !ok { + return true, "" + } + // the presence of key in a diff means that its value has been changed, therefore + // we should fail the precondition. + _, ok = m[key] + if ok { + return false, key + " should not be changed\n" + } else { + return true, "" + } + } +} + +// RequireKeyUnchanged creates a precondition function that fails +// if the metadata.key is present in the diff (indicating its value +// has changed). +func RequireMetadataKeyUnchanged(key string) PreconditionFunc { + return func(diff interface{}) (bool, string) { + m, ok := diff.(map[string]interface{}) + if !ok { + return true, "" + } + m1, ok := m["metadata"] + if !ok { + return true, "" + } + m2, ok := m1.(map[string]interface{}) + if !ok { + return true, "" + } + _, ok = m2[key] + if ok { + return false, "metadata." + key + " should not be changed\n" + } else { + return true, "" + } + } +} + +// TestPreconditions test if preconditions hold given the edit +func TestPreconditionsHold(edit []byte, preconditions []PreconditionFunc) (bool, string) { + diff := make(map[string]interface{}) + if err := json.Unmarshal(edit, &diff); err != nil { + return false, err.Error() + } + for _, fn := range preconditions { + if hold, msg := fn(diff); !hold { + return false, msg + } + } + return true, "" +} + +// NewDelta accepts two JSON or YAML documents and calculates the difference +// between them. It returns a Delta object which can be used to resolve +// conflicts against a third version with a common parent, or an error +// if either document is in error. +func NewDelta(from, to []byte) (*Delta, error) { + d := &Delta{} + before, err := yaml.ToJSON(from) + if err != nil { + return nil, err + } + after, err := yaml.ToJSON(to) + if err != nil { + return nil, err + } + diff, err := jsonpatch.CreateMergePatch(before, after) + if err != nil { + return nil, err + } + glog.V(6).Infof("Patch created from:\n%s\n%s\n%s", string(before), string(after), string(diff)) + d.original = before + d.edit = diff + return d, nil +} + +// Apply attempts to apply the changes described by Delta onto latest, +// returning an error if the changes cannot be applied cleanly. +// IsConflicting will be true if the changes overlap, otherwise a +// generic error will be returned. +func (d *Delta) Apply(latest []byte) ([]byte, error) { + base, err := yaml.ToJSON(latest) + if err != nil { + return nil, err + } + changes, err := jsonpatch.CreateMergePatch(d.original, base) + if err != nil { + return nil, err + } + diff1 := make(map[string]interface{}) + if err := json.Unmarshal(d.edit, &diff1); err != nil { + return nil, err + } + diff2 := make(map[string]interface{}) + if err := json.Unmarshal(changes, &diff2); err != nil { + return nil, err + } + for _, fn := range d.preconditions { + hold1, _ := fn(diff1) + hold2, _ := fn(diff2) + if !hold1 || !hold2 { + return nil, ErrPreconditionFailed + } + } + + glog.V(6).Infof("Testing for conflict between:\n%s\n%s", string(d.edit), string(changes)) + if hasConflicts(diff1, diff2) { + return nil, ErrConflict + } + return jsonpatch.MergePatch(base, d.edit) +} + +// IsConflicting returns true if the provided error indicates a +// conflict exists between the original changes and the applied +// changes. +func IsConflicting(err error) bool { + return err == ErrConflict +} + +// IsPreconditionFailed returns true if the provided error indicates +// a Delta precondition did not succeed. +func IsPreconditionFailed(err error) bool { + return err == ErrPreconditionFailed +} + +var ErrPreconditionFailed = fmt.Errorf("a precondition failed") +var ErrConflict = fmt.Errorf("changes are in conflict") + +// hasConflicts returns true if the left and right JSON interface objects overlap with +// different values in any key. The code will panic if an unrecognized type is passed +// (anything not returned by a JSON decode). All keys are required to be strings. +func hasConflicts(left, right interface{}) bool { + switch typedLeft := left.(type) { + case map[string]interface{}: + switch typedRight := right.(type) { + case map[string]interface{}: + for key, leftValue := range typedLeft { + if rightValue, ok := typedRight[key]; ok && hasConflicts(leftValue, rightValue) { + return true + } + } + return false + default: + return true + } + case []interface{}: + switch typedRight := right.(type) { + case []interface{}: + if len(typedLeft) != len(typedRight) { + return true + } + for i := range typedLeft { + if hasConflicts(typedLeft[i], typedRight[i]) { + return true + } + } + return false + default: + return true + } + case string, float64, bool, int, int64, nil: + return !reflect.DeepEqual(left, right) + default: + panic(fmt.Sprintf("unknown type: %v", reflect.TypeOf(left))) + } +} + +func (d *Delta) Edit() []byte { + return d.edit +} diff --git a/pkg/kubectl/cmd/util/jsonmerge/jsonmerge_test.go b/pkg/kubectl/cmd/util/jsonmerge/jsonmerge_test.go new file mode 100644 index 00000000000..2c9559a70f0 --- /dev/null +++ b/pkg/kubectl/cmd/util/jsonmerge/jsonmerge_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jsonmerge + +import ( + "testing" +) + +func TestHasConflicts(t *testing.T) { + testCases := []struct { + A interface{} + B interface{} + Ret bool + }{ + {A: "hello", B: "hello", Ret: false}, // 0 + {A: "hello", B: "hell", Ret: true}, + {A: "hello", B: nil, Ret: true}, + {A: "hello", B: 1, Ret: true}, + {A: "hello", B: float64(1.0), Ret: true}, + {A: "hello", B: false, Ret: true}, + + {A: "hello", B: []interface{}{}, Ret: true}, // 6 + {A: []interface{}{1}, B: []interface{}{}, Ret: true}, + {A: []interface{}{}, B: []interface{}{}, Ret: false}, + {A: []interface{}{1}, B: []interface{}{1}, Ret: false}, + {A: map[string]interface{}{}, B: []interface{}{1}, Ret: true}, + + {A: map[string]interface{}{}, B: map[string]interface{}{"a": 1}, Ret: false}, // 11 + {A: map[string]interface{}{"a": 1}, B: map[string]interface{}{"a": 1}, Ret: false}, + {A: map[string]interface{}{"a": 1}, B: map[string]interface{}{"a": 2}, Ret: true}, + {A: map[string]interface{}{"a": 1}, B: map[string]interface{}{"b": 2}, Ret: false}, + + { // 15 + A: map[string]interface{}{"a": []interface{}{1}}, + B: map[string]interface{}{"a": []interface{}{1}}, + Ret: false, + }, + { + A: map[string]interface{}{"a": []interface{}{1}}, + B: map[string]interface{}{"a": []interface{}{}}, + Ret: true, + }, + { + A: map[string]interface{}{"a": []interface{}{1}}, + B: map[string]interface{}{"a": 1}, + Ret: true, + }, + } + + for i, testCase := range testCases { + out := hasConflicts(testCase.A, testCase.B) + if out != testCase.Ret { + t.Errorf("%d: expected %t got %t", i, testCase.Ret, out) + continue + } + out = hasConflicts(testCase.B, testCase.A) + if out != testCase.Ret { + t.Errorf("%d: expected reversed %t got %t", i, testCase.Ret, out) + } + } +}