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

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

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 类型,然后抛出自己更有用的错误。你可以根据你的用例扩展这个通用思路。

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 格式化符号非常有用。

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

下面的正则表达式表示:

  1. ^\W* 以任意非字母数字字符开头
  2. (\w+) 捕获后面的字母数字字符串
  3. \W*\s? 匹配非字母数字字符
  4. (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:

  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.

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:

  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"
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:

https://goplay.tools/snippet/bcZO7wa8Vnl

答案2

得分: 1

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

解释:

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

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

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

  1. 第一个错误:
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
}
  1. 第二个错误:
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
}
  1. 正常返回:
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:

  1. 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
}
  1. 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
}
  1. 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 ""

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:

确定