Commit b3425910 authored by k8s-merge-robot's avatar k8s-merge-robot

Merge pull request #13604 from JanetKuo/kubectl-edit

Auto commit by PR queue bot
parents e18c9639 5a608446
......@@ -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
......
......@@ -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")
......
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
.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!
......@@ -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
......
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
......@@ -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
......
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
<strong>
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).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
## 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
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_edit.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->
......@@ -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
......
......@@ -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))
......
/*
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)
}
/*
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)
}
}
// +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}
// +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}
/*
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
}
/*
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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment