From ce73088a718c30d8a3577f5d0521584b9c201e69 Mon Sep 17 00:00:00 2001 From: John Kelly Date: Wed, 4 Oct 2017 18:01:49 -0400 Subject: [PATCH 1/3] client-go: Update CRUD example This updates the create-update-delete-deployment example with the following: Add rollback step to demonstrate rolling back deployments with client-go. Modify the for-loops used in both Update steps to Get() the latest version of the Deployment from the server before attempting Update(). This is necessary because the object returned by Create() does not have the new resourceVersion, causing the initial Update() to always fail due to conflicting resource versions. Putting the Get() at the top of the loop seems to fix this bug. Make -kubeconfig flag optional if config is in default location, using the same method found in the out-of-cluster example. Patch is motivated by effort to improve client-go examples. Signed-off-by: John Kelly --- .../create-update-delete-deployment/README.md | 21 ++++-- .../create-update-delete-deployment/main.go | 73 +++++++++++++++---- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md index 6b7ed4eac98..7bf30576071 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md @@ -22,21 +22,28 @@ go build -o ./app Now, run this application on your workstation with your local kubeconfig file: ``` +./app +# or specify a kubeconfig file with flag ./app -kubeconfig=$HOME/.kube/config ``` Running this command will execute the following operations on your cluster: -1. **Create Deployment:** This will create a 2 replica Deployment. Verify with - `kubectl get pods`. +1. **Create Deployment:** This will create a 2 replica Deployment with + annotation `fizz=buzz`. Verify with `kubectl get pods`. 2. **Update Deployment:** This will update the Deployment resource created in - previous step to set the replica count to 1 and add annotations. You are + previous step to set the replica count to 1 and update annotations. You are encouraged to inspect the retry loop that handles conflicts. Verify the new replica count and `foo=bar` annotation with `kubectl describe deployment demo`. -3. **List Deployments:** This will retrieve Deployments in the `default` +3. **Rollback Deployment:** This will rollback the Deployment to the last + revision, in this case the revision created in Step 1. Use `kubectl describe` + to verify the original annotation `fizz=buzz`. Also note the replica count + is still 1; this is because a Deployment revision is created if and only + if the Deployment's pod template (`.spec.template`) is changed. +4. **List Deployments:** This will retrieve Deployments in the `default` namespace and print their names and replica counts. -4. **Delete Deployment:** This will delete the Deployment object and its +5. **Delete Deployment:** This will delete the Deployment object and its dependent ReplicaSet resource. Verify with `kubectl get deployments`. Each step is separated by an interactive prompt. You must hit the @@ -55,6 +62,10 @@ Updating deployment... Updated deployment... -> Press Return key to continue. +Rolling back deployment... +Rolled back deployment... +-> Press Return key to continue. + Listing deployments in namespace "default": * demo-deployment (1 replicas) -> Press Return key to continue. diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go index 722a39458cd..e026c7e926c 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go @@ -22,6 +22,7 @@ import ( "flag" "fmt" "os" + "path/filepath" appsv1beta1 "k8s.io/api/apps/v1beta1" apiv1 "k8s.io/api/core/v1" @@ -34,11 +35,14 @@ import ( ) func main() { - kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file") - flag.Parse() - if *kubeconfig == "" { - panic("-kubeconfig not specified") + var kubeconfig *string + if home := homeDir(); home != "" { + kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } else { + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") } + flag.Parse() + config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) if err != nil { panic(err) @@ -61,6 +65,9 @@ func main() { Labels: map[string]string{ "app": "demo", }, + Annotations: map[string]string{ + "fizz": "buzz", + }, }, Spec: apiv1.PodSpec{ Containers: []apiv1.Container{ @@ -100,24 +107,30 @@ func main() { // 2. Modify the "result" returned by Create()/Get() and retry Update(result) until // you no longer get a conflict error. This way, you can preserve changes made // by other clients between Create() and Update(). This is implemented below: + // + // See the API Conventions: + // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency for { + // Retrieve latest version of Deployment before modifying and updating + result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) + if err != nil { + panic(fmt.Errorf("Get failed: %+v", err)) + } result.Spec.Replicas = int32Ptr(1) // reduce replica count result.Spec.Template.Annotations = map[string]string{ // add annotations "foo": "bar", } - if _, err := deploymentsClient.Update(result); errors.IsConflict(err) { - // Deployment is modified in the meanwhile, query the latest version - // and modify the retrieved object. - fmt.Println("encountered conflict, retrying") - result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) - if err != nil { - panic(fmt.Errorf("Get failed: %+v", err)) + if _, err = deploymentsClient.Update(result); err != nil { + if errors.IsConflict(err) { + // Deployment was modified since last retrieved, need to retry + fmt.Println("Conflicting resource versions, retrying") + } else { + panic(err) } - } else if err != nil { - panic(err) } else { + fmt.Println("Updated deployment...") break } @@ -125,7 +138,32 @@ func main() { // exhausting the apiserver, and add a limit/timeout on the retries to // avoid getting stuck in this loop indefintiely. } - fmt.Println("Updated deployment...") + + // Rollback Deployment + prompt() + fmt.Println("Rolling back deployment...") + + // Use same method as above to avoid version conflicts + for { + result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) + if err != nil { + panic(fmt.Errorf("Get failed: %+v", err)) + } + result.Spec.RollbackTo = &appsv1beta1.RollbackConfig{ + Revision: 0, // Can be specific revision number or 0 for last revision + } + + if _, err = deploymentsClient.Update(result); err != nil { + if errors.IsConflict(err) { + fmt.Println("Conflicting resource versions, retrying") + } else { + panic(err) + } + } else { + fmt.Println("Rolled back deployment...") + break + } + } // List Deployments prompt() @@ -163,3 +201,10 @@ func prompt() { } func int32Ptr(i int32) *int32 { return &i } + +func homeDir() string { + if h := os.Getenv("HOME"); h != "" { + return h + } + return os.Getenv("USERPROFILE") // windows +} From 94f5bcf6f77d5b35074dfab47b5de37096d8ee00 Mon Sep 17 00:00:00 2001 From: John Kelly Date: Thu, 5 Oct 2017 19:07:57 -0400 Subject: [PATCH 2/3] client-go: use retry util in CRUD example This updates the create-update-delete-deployment example with the following: Make use of client-go retry util in Update() steps instead of simple for loops. Using RetryOnConflict is generally better practice as it won't become stuck in a retry loop and uses exponential backoff to prevent exhausting the apiserver. Instead of changing annotations to demonstrate Updates/Rollbacks, change the container image as it is less confusing for readers and a better real-world example. Improve comments and README to reflect above changes. Signed-off-by: John Kelly --- .../create-update-delete-deployment/BUILD | 2 +- .../create-update-delete-deployment/README.md | 21 +++--- .../create-update-delete-deployment/main.go | 71 +++++++------------ 3 files changed, 39 insertions(+), 55 deletions(-) diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD index 106adfe44b3..10839cfb955 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD @@ -19,10 +19,10 @@ go_library( deps = [ "//vendor/k8s.io/api/apps/v1beta1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", + "//vendor/k8s.io/client-go/util/retry:go_default_library", ], ) diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md index 7bf30576071..e9c11a9355b 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/README.md @@ -29,18 +29,19 @@ Now, run this application on your workstation with your local kubeconfig file: Running this command will execute the following operations on your cluster: -1. **Create Deployment:** This will create a 2 replica Deployment with - annotation `fizz=buzz`. Verify with `kubectl get pods`. +1. **Create Deployment:** This will create a 2 replica Deployment. Verify with + `kubectl get pods`. 2. **Update Deployment:** This will update the Deployment resource created in - previous step to set the replica count to 1 and update annotations. You are - encouraged to inspect the retry loop that handles conflicts. Verify the new - replica count and `foo=bar` annotation with `kubectl describe deployment - demo`. + previous step by setting the replica count to 1 and changing the container + image to `nginx:1.13`. You are encouraged to inspect the retry loop that + handles conflicts. Verify the new replica count and container image with + `kubectl describe deployment demo`. 3. **Rollback Deployment:** This will rollback the Deployment to the last - revision, in this case the revision created in Step 1. Use `kubectl describe` - to verify the original annotation `fizz=buzz`. Also note the replica count - is still 1; this is because a Deployment revision is created if and only - if the Deployment's pod template (`.spec.template`) is changed. + revision. In this case, it's the revision that was created in Step 1. + Use `kubectl describe` to verify the container image is now `nginx:1.12`. + Also note that the Deployment's replica count is still 1; this is because a + Deployment revision is created if and only if the Deployment's pod template + (`.spec.template`) is changed. 4. **List Deployments:** This will retrieve Deployments in the `default` namespace and print their names and replica counts. 5. **Delete Deployment:** This will delete the Deployment object and its diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go index e026c7e926c..ab866d7c210 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go @@ -26,10 +26,10 @@ import ( appsv1beta1 "k8s.io/api/apps/v1beta1" apiv1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/retry" // Uncomment the following line to load the gcp plugin (only required to authenticate against GKE clusters). // _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ) @@ -65,15 +65,12 @@ func main() { Labels: map[string]string{ "app": "demo", }, - Annotations: map[string]string{ - "fizz": "buzz", - }, }, Spec: apiv1.PodSpec{ Containers: []apiv1.Container{ { Name: "web", - Image: "nginx:1.13", + Image: "nginx:1.12", Ports: []apiv1.ContainerPort{ { Name: "http", @@ -104,66 +101,52 @@ func main() { // 1. Modify the "deployment" variable and call: Update(deployment). // This works like the "kubectl replace" command and it overwrites/loses changes // made by other clients between you Create() and Update() the object. - // 2. Modify the "result" returned by Create()/Get() and retry Update(result) until + // 2. Modify the "result" returned by Get() and retry Update(result) until // you no longer get a conflict error. This way, you can preserve changes made - // by other clients between Create() and Update(). This is implemented below: + // by other clients between Create() and Update(). This is implemented below + // using the retry utility package included with client-go. (RECOMMENDED) // - // See the API Conventions: + // More Info: // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency - for { - // Retrieve latest version of Deployment before modifying and updating + err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) { + // Retrieve the latest version of Deployment before attempting update + // RetryOnConflict uses exponential backoff to avoid exhausting the apiserver result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) if err != nil { panic(fmt.Errorf("Get failed: %+v", err)) } - result.Spec.Replicas = int32Ptr(1) // reduce replica count - result.Spec.Template.Annotations = map[string]string{ // add annotations - "foo": "bar", - } - if _, err = deploymentsClient.Update(result); err != nil { - if errors.IsConflict(err) { - // Deployment was modified since last retrieved, need to retry - fmt.Println("Conflicting resource versions, retrying") - } else { - panic(err) - } - } else { - fmt.Println("Updated deployment...") - break - } - - // TODO: You should sleep here with an exponential backoff to avoid - // exhausting the apiserver, and add a limit/timeout on the retries to - // avoid getting stuck in this loop indefintiely. + result.Spec.Replicas = int32Ptr(1) // reduce replica count + result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version + result, err = deploymentsClient.Update(result) + return + }) + if err != nil { + panic(fmt.Errorf("Update failed: %+v", err)) } + fmt.Println("Updated deployment...") // Rollback Deployment prompt() fmt.Println("Rolling back deployment...") - - // Use same method as above to avoid version conflicts - for { + // Once again use RetryOnConflict to avoid update conflicts + err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) { result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) if err != nil { panic(fmt.Errorf("Get failed: %+v", err)) } - result.Spec.RollbackTo = &appsv1beta1.RollbackConfig{ - Revision: 0, // Can be specific revision number or 0 for last revision - } - if _, err = deploymentsClient.Update(result); err != nil { - if errors.IsConflict(err) { - fmt.Println("Conflicting resource versions, retrying") - } else { - panic(err) - } - } else { - fmt.Println("Rolled back deployment...") - break + result.Spec.RollbackTo = &appsv1beta1.RollbackConfig{ + Revision: 0, // can be specific revision number, or 0 for last revision } + result, err = deploymentsClient.Update(result) + return + }) + if err != nil { + panic(fmt.Errorf("Rollback failed: %+v", err)) } + fmt.Println("Rolled back deployment...") // List Deployments prompt() From e71c9f1b4beb06bcd3694163e6bf1764fb49edd6 Mon Sep 17 00:00:00 2001 From: John Kelly Date: Sun, 8 Oct 2017 13:28:22 -0400 Subject: [PATCH 3/3] client-go: fix err scope in CRUD example This fixes some scope isses that were introduced by shadowing vars inside anonymous functions as well as using a naked return. Fixed by using unique err names and explicitly returning errors. Additional improvement is using the HomeDir() util function provided by client-go instead of including a helper function at the bottom of this example. Signed-off-by: John Kelly --- .../create-update-delete-deployment/BUILD | 1 + .../create-update-delete-deployment/main.go | 42 ++++++++----------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD index 10839cfb955..741403b314a 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/BUILD @@ -22,6 +22,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", + "//vendor/k8s.io/client-go/util/homedir:go_default_library", "//vendor/k8s.io/client-go/util/retry:go_default_library", ], ) diff --git a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go index ab866d7c210..f6d622745f9 100644 --- a/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go +++ b/staging/src/k8s.io/client-go/examples/create-update-delete-deployment/main.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" "k8s.io/client-go/util/retry" // Uncomment the following line to load the gcp plugin (only required to authenticate against GKE clusters). // _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" @@ -36,7 +37,7 @@ import ( func main() { var kubeconfig *string - if home := homeDir(); home != "" { + if home := homedir.HomeDir(); home != "" { kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") } else { kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") @@ -109,21 +110,21 @@ func main() { // More Info: // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency - err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) { + retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { // Retrieve the latest version of Deployment before attempting update // RetryOnConflict uses exponential backoff to avoid exhausting the apiserver - result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) - if err != nil { - panic(fmt.Errorf("Get failed: %+v", err)) + result, getErr := deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) + if getErr != nil { + panic(fmt.Errorf("Failed to get latest version of Deployment: %v", getErr)) } result.Spec.Replicas = int32Ptr(1) // reduce replica count result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version - result, err = deploymentsClient.Update(result) - return + _, updateErr := deploymentsClient.Update(result) + return updateErr }) - if err != nil { - panic(fmt.Errorf("Update failed: %+v", err)) + if retryErr != nil { + panic(fmt.Errorf("Update failed: %v", retryErr)) } fmt.Println("Updated deployment...") @@ -131,20 +132,20 @@ func main() { prompt() fmt.Println("Rolling back deployment...") // Once again use RetryOnConflict to avoid update conflicts - err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) { - result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) - if err != nil { - panic(fmt.Errorf("Get failed: %+v", err)) + retryErr = retry.RetryOnConflict(retry.DefaultRetry, func() error { + result, getErr := deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) + if getErr != nil { + panic(fmt.Errorf("Failed to get latest version of Deployment: %v", getErr)) } result.Spec.RollbackTo = &appsv1beta1.RollbackConfig{ Revision: 0, // can be specific revision number, or 0 for last revision } - result, err = deploymentsClient.Update(result) - return + _, updateErr := deploymentsClient.Update(result) + return updateErr }) - if err != nil { - panic(fmt.Errorf("Rollback failed: %+v", err)) + if retryErr != nil { + panic(fmt.Errorf("Rollback failed: %v", retryErr)) } fmt.Println("Rolled back deployment...") @@ -184,10 +185,3 @@ func prompt() { } func int32Ptr(i int32) *int32 { return &i } - -func homeDir() string { - if h := os.Getenv("HOME"); h != "" { - return h - } - return os.Getenv("USERPROFILE") // windows -}