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 <jekohk@gmail.com>

Kubernetes-commit: 94f5bcf6f77d5b35074dfab47b5de37096d8ee00
This commit is contained in:
John Kelly 2017-10-05 19:07:57 -04:00 committed by Kubernetes Publisher
parent 4e80b27156
commit ddf6c35ca5
3 changed files with 39 additions and 55 deletions

View File

@ -19,10 +19,10 @@ go_library(
deps = [ deps = [
"//vendor/k8s.io/api/apps/v1beta1:go_default_library", "//vendor/k8s.io/api/apps/v1beta1:go_default_library",
"//vendor/k8s.io/api/core/v1: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/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes: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/tools/clientcmd:go_default_library",
"//vendor/k8s.io/client-go/util/retry:go_default_library",
], ],
) )

View File

@ -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: Running this command will execute the following operations on your cluster:
1. **Create Deployment:** This will create a 2 replica Deployment with 1. **Create Deployment:** This will create a 2 replica Deployment. Verify with
annotation `fizz=buzz`. Verify with `kubectl get pods`. `kubectl get pods`.
2. **Update Deployment:** This will update the Deployment resource created in 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 previous step by setting the replica count to 1 and changing the container
encouraged to inspect the retry loop that handles conflicts. Verify the new image to `nginx:1.13`. You are encouraged to inspect the retry loop that
replica count and `foo=bar` annotation with `kubectl describe deployment handles conflicts. Verify the new replica count and container image with
demo`. `kubectl describe deployment demo`.
3. **Rollback Deployment:** This will rollback the Deployment to the last 3. **Rollback Deployment:** This will rollback the Deployment to the last
revision, in this case the revision created in Step 1. Use `kubectl describe` revision. In this case, it's the revision that was created in Step 1.
to verify the original annotation `fizz=buzz`. Also note the replica count Use `kubectl describe` to verify the container image is now `nginx:1.12`.
is still 1; this is because a Deployment revision is created if and only Also note that the Deployment's replica count is still 1; this is because a
if the Deployment's pod template (`.spec.template`) is changed. 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` 4. **List Deployments:** This will retrieve Deployments in the `default`
namespace and print their names and replica counts. namespace and print their names and replica counts.
5. **Delete Deployment:** This will delete the Deployment object and its 5. **Delete Deployment:** This will delete the Deployment object and its

View File

@ -26,10 +26,10 @@ import (
appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta1 "k8s.io/api/apps/v1beta1"
apiv1 "k8s.io/api/core/v1" apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd" "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). // 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" // _ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
) )
@ -65,15 +65,12 @@ func main() {
Labels: map[string]string{ Labels: map[string]string{
"app": "demo", "app": "demo",
}, },
Annotations: map[string]string{
"fizz": "buzz",
},
}, },
Spec: apiv1.PodSpec{ Spec: apiv1.PodSpec{
Containers: []apiv1.Container{ Containers: []apiv1.Container{
{ {
Name: "web", Name: "web",
Image: "nginx:1.13", Image: "nginx:1.12",
Ports: []apiv1.ContainerPort{ Ports: []apiv1.ContainerPort{
{ {
Name: "http", Name: "http",
@ -104,66 +101,52 @@ func main() {
// 1. Modify the "deployment" variable and call: Update(deployment). // 1. Modify the "deployment" variable and call: Update(deployment).
// This works like the "kubectl replace" command and it overwrites/loses changes // This works like the "kubectl replace" command and it overwrites/loses changes
// made by other clients between you Create() and Update() the object. // 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 // 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 // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency
for { err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
// Retrieve latest version of Deployment before modifying and updating // 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{}) result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{})
if err != nil { if err != nil {
panic(fmt.Errorf("Get failed: %+v", err)) panic(fmt.Errorf("Get failed: %+v", err))
} }
result.Spec.Replicas = int32Ptr(1) // reduce replica count result.Spec.Replicas = int32Ptr(1) // reduce replica count
result.Spec.Template.Annotations = map[string]string{ // add annotations result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version
"foo": "bar", result, err = deploymentsClient.Update(result)
return
})
if err != nil {
panic(fmt.Errorf("Update 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 {
fmt.Println("Updated deployment...") 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.
}
// Rollback Deployment // Rollback Deployment
prompt() prompt()
fmt.Println("Rolling back deployment...") fmt.Println("Rolling back deployment...")
// Once again use RetryOnConflict to avoid update conflicts
// Use same method as above to avoid version conflicts err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
for {
result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{}) result, err = deploymentsClient.Get("demo-deployment", metav1.GetOptions{})
if err != nil { if err != nil {
panic(fmt.Errorf("Get failed: %+v", err)) 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 { result.Spec.RollbackTo = &appsv1beta1.RollbackConfig{
if errors.IsConflict(err) { Revision: 0, // can be specific revision number, or 0 for last revision
fmt.Println("Conflicting resource versions, retrying") }
} else { result, err = deploymentsClient.Update(result)
panic(err) return
})
if err != nil {
panic(fmt.Errorf("Rollback failed: %+v", err))
} }
} else {
fmt.Println("Rolled back deployment...") fmt.Println("Rolled back deployment...")
break
}
}
// List Deployments // List Deployments
prompt() prompt()