英文:
How to return a custom user friendly error message in Kubernetes?
问题
我有一个使用Golang与Kubernetes通信的后端。我想重新构造从Kubernetes获取的错误响应,并将其发送到前端。
当用户添加一个无效的名称或已存在的名称时,我希望返回有意义的验证错误消息...
而且我希望是通用的,而不是在每个端点的控制器中硬编码。
我正在使用kubernetes/client-go
。
- 第一个错误:
例如,假设我想将一个酒店添加到etcd
中,当我尝试添加酒店的名称:hotel123时,它已经存在。
- 我得到这个错误消息:
\"hotel123\" already exists
。 - 我想要的是:
hotel123 already exists
。
- 第二个错误:
例如,假设我想将一个酒店添加到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
.
- 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
.
- 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
通常情况下(虽然有一些变通方法),如果你想捕获错误以返回更有用的错误信息,你需要确保满足以下条件:
下面的示例中,我试图读取一个不存在的配置文件。我的代码检查返回的错误是否为 fs.PathError
类型,然后抛出自己更有用的错误。你可以根据你的用例扩展这个通用思路。
package main
import (
"errors"
"fmt"
"io/fs"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
var myError error
config, originalError := clientcmd.BuildConfigFromFlags("", "/some/path/that/doesnt/exist")
if originalError != nil {
var pathError *fs.PathError
switch {
case errors.As(originalError, &pathError):
myError = fmt.Errorf("there is no config file at %s", originalError.(*fs.PathError).Path)
default:
myError = fmt.Errorf("there was an error and its type was %T", originalError)
}
fmt.Printf("%#v", myError)
} else {
fmt.Println("There was no error")
fmt.Println(config)
}
}
在调试过程中,你会发现 %T
格式化符号非常有用。
对于你的特定用例,你可以使用正则表达式来解析所需的文本。
下面的正则表达式表示:
^\W*
以任意非字母数字字符开头(\w+)
捕获后面的字母数字字符串\W*\s?
匹配非字母数字字符(is\sinvalid)
捕获 "is invalid"
func MyError(inError error) error {
pattern, _ := regexp.Compile(`^\W*(\w+)\W*\s?(is\sinvalid)(.*)$`)
myErrorString := pattern.ReplaceAll([]byte(inError.Error()), []byte("$1 $2"))
return errors.New(string(myErrorString))
}
在这个 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:
- The error you're trapping has a meaningful type
- 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.
package main
import (
"errors"
"fmt"
"io/fs"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
var myError error
config, originalError := clientcmd.BuildConfigFromFlags("", "/some/path/that/doesnt/exist")
if originalError != nil {
var pathError *fs.PathError
switch {
case errors.As(originalError, &pathError):
myError = fmt.Errorf("there is no config file at %s", originalError.(*fs.PathError).Path)
default:
myError = fmt.Errorf("there was an error and it's type was %T", originalError)
}
fmt.Printf("%#v", myError)
} else {
fmt.Println("There was no error")
fmt.Println(config)
}
}
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:
^\W*
start with any non-alhpanumeric characters(\w+)
capture the alphanumeric string following\W*\s?
match non-alphanumeric characters(is\sinvalid)
capture "is invalid"
func MyError(inError error) error {
pattern, _ := regexp.Compile(`^\W*(\w+)\W*\s?(is\sinvalid)(.*)$`)
myErrorString := pattern.ReplaceAll([]byte(inError.Error()), []byte("$1 $2"))
return errors.New(string(myErrorString))
}
As seen on this playground:
答案2
得分: 1
err.Error()
是从Kubernetes服务器获取的原始、有意义且最好的错误消息,用于向用户提供(或者您可以自己翻译)。
解释:
您需要深入了解kubernetes/client-go
客户端库。
每个客户端通过HTTP REST API与K8s服务器进行通信,服务器以json
格式返回响应。如果可能的话,client-go
库会解码响应体并将结果存储到对象中。
对于您的情况,让我通过Namespace
资源给您举几个例子:
- 第一个错误:
POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
响应状态:409 Conflict
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "namespaces \"hotel123\" already exists",
"reason": "AlreadyExists",
"details": {
"name": "hotel123",
"kind": "namespaces"
},
"code": 409
}
- 第二个错误:
POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
响应状态:422 Unprocessable Entity
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"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])?')",
"reason": "Invalid",
"details": {
"name": "hotel_123",
"kind": "Namespace",
"causes": [
{
"reason": "FieldValueInvalid",
"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])?')",
"field": "metadata.name"
}
]
},
"code": 422
}
- 正常返回:
POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
响应状态:201 Created
{
"kind": "Namespace",
"apiVersion": "v1",
"metadata": {
"name": "hotel12345",
"uid": "7a301d8b-37cd-45a5-8345-82wsufy88223456",
"resourceVersion": "12233445566",
"creationTimestamp": "2023-04-03T15:35:59Z",
"managedFields": [
{
"manager": "kubectl-create",
"operation": "Update",
"apiVersion": "v1",
"time": "2023-04-03T15:35:59Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:status": {
"f:phase": {}
}
}
}
]
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
}
简而言之,如果HTTP状态不是2xx,返回的对象类型是Status且.Status != StatusSuccess,则会使用Status中的附加信息(在本例中为message
)来丰富错误,就像下面的代码片段一样:
createdNamespace, err := clientset.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{})
if err != nil {
// 打印“namespaces "hotel123" already exists”或类似的消息
fmt.Println(err.Error())
return err.Error()
}
fmt.Printf("在集群中创建了Namespace %+v\n", createdNamespace)
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:
- First error:
POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
Response Status: 409 Conflict
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "namespaces \"hotel123\" already exists",
"reason": "AlreadyExists",
"details": {
"name": "hotel123",
"kind": "namespaces"
},
"code": 409
}
- second error:
POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
Response Status: 422 Unprocessable Entity
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"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])?')",
"reason": "Invalid",
"details": {
"name": "hotel_123",
"kind": "Namespace",
"causes": [
{
"reason": "FieldValueInvalid",
"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])?')",
"field": "metadata.name"
}
]
},
"code": 422
}
- normal return:
POST https://xxx.xx.xx.xx:6443/api/v1/namespaces?fieldManager=kubectl-create
Response Status: 201 Created
{
"kind": "Namespace",
"apiVersion": "v1",
"metadata": {
"name": "hotel12345",
"uid": "7a301d8b-37cd-45a5-8345-82wsufy88223456",
"resourceVersion": "12233445566",
"creationTimestamp": "2023-04-03T15:35:59Z",
"managedFields": [
{
"manager": "kubectl-create",
"operation": "Update",
"apiVersion": "v1",
"time": "2023-04-03T15:35:59Z",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:status": {
"f:phase": {}
}
}
}
]
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
}
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:
createdNamespace, err := clientset.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{})
if err != nil {
// print "namespaces \"hotel123\" already exists" or so
fmt.Println(err.Error())
return err.Error()
}
fmt.Printf("Created Namespace %+v in the cluster\n", createdNamespace)
return ""
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论