How to return a custom user friendly error message in Kubernetes?

huangapple go评论118阅读模式
英文:

How to return a custom user friendly error message in Kubernetes?

问题

我有一个使用Golang与Kubernetes通信的后端。我想重新构造从Kubernetes获取的错误响应,并将其发送到前端。

当用户添加一个无效的名称或已存在的名称时,我希望返回有意义的验证错误消息...

而且我希望是通用的,而不是在每个端点的控制器中硬编码。

我正在使用kubernetes/client-go

  1. 第一个错误

例如,假设我想将一个酒店添加到etcd中,当我尝试添加酒店的名称:hotel123时,它已经存在。

  • 我得到这个错误消息:\"hotel123\" already exists
  • 我想要的是:hotel123 already exists
  1. 第二个错误

例如,假设我想将一个酒店添加到etcd中,当我尝试添加酒店的名称:hotel_123时,它已经存在。

  • 我得到这个错误消息:\"hotel_123\" is invalid, Invalid value: \"hotel_123\"...
  • 我想要的是:hotel_123 is invalid

如何返回自定义的用户友好错误消息?

PS:我有多个函数,所以验证应该是通用的。

英文:

I have a backend with golang that talks to k8s. I want to reformulate the error response that i get from k8s and send it to the frontend.

I want to return a meaningful validation error messages for the user, when he add a non valid name, something already exist ...

And i want something generic not hardcoded in each endpoint's controller.

I am using kubernetes/client-go.

  1. First error:

For example lets say i want to add a hotel to the etcd, when i try to add the hotel's name: hotel123, that's already exist.

  • I get this error message: \"hotel123\" already exists.
  • What i want : hotel123 already exists.
  1. second error:

For example lets say i want to add a hotel to the etcd, when i try to add the hotel name: hotel_123, that's alerady exist.

  • I get this error message: \"hotel_123\" is invalid, Invalid value: \"hotel_123\"...
  • What i want: hotel_123 is invalid

How to return a custom user friendly error message ?

PS: i have multiple functions, so the validation should be generic.

答案1

得分: 1

通常情况下(虽然有一些变通方法),如果你想捕获错误以返回更有用的错误信息,你需要确保满足以下条件:

  1. 你要捕获的错误具有有意义的类型
  2. 你使用的 Go 版本 >= 1.13,其中包含了有用的辅助函数

下面的示例中,我试图读取一个不存在的配置文件。我的代码检查返回的错误是否为 fs.PathError 类型,然后抛出自己更有用的错误。你可以根据你的用例扩展这个通用思路。

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/fs"
  6. "k8s.io/client-go/tools/clientcmd"
  7. )
  8. func main() {
  9. var myError error
  10. config, originalError := clientcmd.BuildConfigFromFlags("", "/some/path/that/doesnt/exist")
  11. if originalError != nil {
  12. var pathError *fs.PathError
  13. switch {
  14. case errors.As(originalError, &pathError):
  15. myError = fmt.Errorf("there is no config file at %s", originalError.(*fs.PathError).Path)
  16. default:
  17. myError = fmt.Errorf("there was an error and its type was %T", originalError)
  18. }
  19. fmt.Printf("%#v", myError)
  20. } else {
  21. fmt.Println("There was no error")
  22. fmt.Println(config)
  23. }
  24. }

在调试过程中,你会发现 %T 格式化符号非常有用。

对于你的特定用例,你可以使用正则表达式来解析所需的文本。

下面的正则表达式表示:

  1. ^\W* 以任意非字母数字字符开头
  2. (\w+) 捕获后面的字母数字字符串
  3. \W*\s? 匹配非字母数字字符
  4. (is\sinvalid) 捕获 "is invalid"
  1. func MyError(inError error) error {
  2. pattern, _ := regexp.Compile(`^\W*(\w+)\W*\s?(is\sinvalid)(.*)$`)
  3. myErrorString := pattern.ReplaceAll([]byte(inError.Error()), []byte("$1 $2"))
  4. return errors.New(string(myErrorString))
  5. }

在这个 playground 上可以看到示例:

https://goplay.tools/snippet/bcZO7wa8Vnl

英文:

In general (although there are workarounds), if you want to trap an error in order to return a more useful error, you want to ensure the following conditions are met:

  1. The error you're trapping has a meaningful type
  2. You're using go version >= 1.13 which ships with useful helper functions

In the following example I'm trying to read a config file that doesn't exist. My code checks that the error returned is a fs.PathError and then throws it's own more useful error. You can extend this general idea to your use case.

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/fs"
  6. "k8s.io/client-go/tools/clientcmd"
  7. )
  8. func main() {
  9. var myError error
  10. config, originalError := clientcmd.BuildConfigFromFlags("", "/some/path/that/doesnt/exist")
  11. if originalError != nil {
  12. var pathError *fs.PathError
  13. switch {
  14. case errors.As(originalError, &pathError):
  15. myError = fmt.Errorf("there is no config file at %s", originalError.(*fs.PathError).Path)
  16. default:
  17. myError = fmt.Errorf("there was an error and it's type was %T", originalError)
  18. }
  19. fmt.Printf("%#v", myError)
  20. } else {
  21. fmt.Println("There was no error")
  22. fmt.Println(config)
  23. }
  24. }

In your debugging, you will find the %T formatter useful.

For your specific use-case, you can use a Regex to parse out the desired text.

The regex below says:

  1. ^\W* start with any non-alhpanumeric characters
  2. (\w+) capture the alphanumeric string following
  3. \W*\s? match non-alphanumeric characters
  4. (is\sinvalid) capture "is invalid"
  1. func MyError(inError error) error {
  2. pattern, _ := regexp.Compile(`^\W*(\w+)\W*\s?(is\sinvalid)(.*)$`)
  3. myErrorString := pattern.ReplaceAll([]byte(inError.Error()), []byte("$1 $2"))
  4. return errors.New(string(myErrorString))
  5. }

As seen on this playground:

https://goplay.tools/snippet/bcZO7wa8Vnl

答案2

得分: 1

err.Error()是从Kubernetes服务器获取的原始、有意义且最好的错误消息,用于向用户提供(或者您可以自己翻译)。

解释:

您需要深入了解kubernetes/client-go客户端库。

每个客户端通过HTTP REST API与K8s服务器进行通信,服务器以json格式返回响应。如果可能的话,client-go库会解码响应体并将结果存储到对象中。

对于您的情况,让我通过Namespace资源给您举几个例子:

  1. 第一个错误:
  1. POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
  2. 响应状态:409 Conflict
  3. {
  4. "kind": "Status",
  5. "apiVersion": "v1",
  6. "metadata": {},
  7. "status": "Failure",
  8. "message": "namespaces \"hotel123\" already exists",
  9. "reason": "AlreadyExists",
  10. "details": {
  11. "name": "hotel123",
  12. "kind": "namespaces"
  13. },
  14. "code": 409
  15. }
  1. 第二个错误:
  1. POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
  2. 响应状态:422 Unprocessable Entity
  3. {
  4. "kind": "Status",
  5. "apiVersion": "v1",
  6. "metadata": {},
  7. "status": "Failure",
  8. "message": "Namespace \"hotel_123\" is invalid: metadata.name: Invalid value: \"hotel_123\": a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]\r\n([-a-z0-9]*[a-z0-9])?')",
  9. "reason": "Invalid",
  10. "details": {
  11. "name": "hotel_123",
  12. "kind": "Namespace",
  13. "causes": [
  14. {
  15. "reason": "FieldValueInvalid",
  16. "message": "Invalid value: \"hotel_123\": a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')",
  17. "field": "metadata.name"
  18. }
  19. ]
  20. },
  21. "code": 422
  22. }
  1. 正常返回:
  1. POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
  2. 响应状态:201 Created
  3. {
  4. "kind": "Namespace",
  5. "apiVersion": "v1",
  6. "metadata": {
  7. "name": "hotel12345",
  8. "uid": "7a301d8b-37cd-45a5-8345-82wsufy88223456",
  9. "resourceVersion": "12233445566",
  10. "creationTimestamp": "2023-04-03T15:35:59Z",
  11. "managedFields": [
  12. {
  13. "manager": "kubectl-create",
  14. "operation": "Update",
  15. "apiVersion": "v1",
  16. "time": "2023-04-03T15:35:59Z",
  17. "fieldsType": "FieldsV1",
  18. "fieldsV1": {
  19. "f:status": {
  20. "f:phase": {}
  21. }
  22. }
  23. }
  24. ]
  25. },
  26. "spec": {
  27. "finalizers": [
  28. "kubernetes"
  29. ]
  30. },
  31. "status": {
  32. "phase": "Active"
  33. }
  34. }

简而言之,如果HTTP状态不是2xx,返回的对象类型是Status且.Status != StatusSuccess,则会使用Status中的附加信息(在本例中为message)来丰富错误,就像下面的代码片段一样:

  1. createdNamespace, err := clientset.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{})
  2. if err != nil {
  3. // 打印“namespaces "hotel123" already exists”或类似的消息
  4. fmt.Println(err.Error())
  5. return err.Error()
  6. }
  7. fmt.Printf("在集群中创建了Namespace %+v\n", createdNamespace)
  8. return ""
英文:

String err.Error() is the original, meaningful and best error message you can get from Kubernetes server for the user (Or you have to translate it by yourself).

Explains:

You need to look beyond the surface of kubernetes/client-go client library.

Each client talks to k8s server through HTTP REST APIs, which sends back response in json. It's the client-go library that decodes the response body and stores the result into object, if possible.

As for your case, let me give you some examples through the Namespace resource:

  1. First error:
  1. POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
  2. Response Status: 409 Conflict
  3. {
  4. "kind": "Status",
  5. "apiVersion": "v1",
  6. "metadata": {},
  7. "status": "Failure",
  8. "message": "namespaces \"hotel123\" already exists",
  9. "reason": "AlreadyExists",
  10. "details": {
  11. "name": "hotel123",
  12. "kind": "namespaces"
  13. },
  14. "code": 409
  15. }
  1. second error:
  1. POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
  2. Response Status: 422 Unprocessable Entity
  3. {
  4. "kind": "Status",
  5. "apiVersion": "v1",
  6. "metadata": {},
  7. "status": "Failure",
  8. "message": "Namespace \"hotel_123\" is invalid: metadata.name: Invalid value: \"hotel_123\": a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]\r\n([-a-z0-9]*[a-z0-9])?')",
  9. "reason": "Invalid",
  10. "details": {
  11. "name": "hotel_123",
  12. "kind": "Namespace",
  13. "causes": [
  14. {
  15. "reason": "FieldValueInvalid",
  16. "message": "Invalid value: \"hotel_123\": a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')",
  17. "field": "metadata.name"
  18. }
  19. ]
  20. },
  21. "code": 422
  22. }
  1. normal return:
  1. POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
  2. Response Status: 201 Created
  3. {
  4. "kind": "Namespace",
  5. "apiVersion": "v1",
  6. "metadata": {
  7. "name": "hotel12345",
  8. "uid": "7a301d8b-37cd-45a5-8345-82wsufy88223456",
  9. "resourceVersion": "12233445566",
  10. "creationTimestamp": "2023-04-03T15:35:59Z",
  11. "managedFields": [
  12. {
  13. "manager": "kubectl-create",
  14. "operation": "Update",
  15. "apiVersion": "v1",
  16. "time": "2023-04-03T15:35:59Z",
  17. "fieldsType": "FieldsV1",
  18. "fieldsV1": {
  19. "f:status": {
  20. "f:phase": {}
  21. }
  22. }
  23. }
  24. ]
  25. },
  26. "spec": {
  27. "finalizers": [
  28. "kubernetes"
  29. ]
  30. },
  31. "status": {
  32. "phase": "Active"
  33. }
  34. }

In a word, if the HTTP Status is not 2xx, the returned object is of type Status and has .Status != StatusSuccess, the additional information(message in this case) in Status will be used to enrich the error, just as the code snippets below:

  1. createdNamespace, err := clientset.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{})
  2. if err != nil {
  3. // print "namespaces \"hotel123\" already exists" or so
  4. fmt.Println(err.Error())
  5. return err.Error()
  6. }
  7. fmt.Printf("Created Namespace %+v in the cluster\n", createdNamespace)
  8. return ""

huangapple
  • 本文由 发表于 2023年3月28日 16:07:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/75863744.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定