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

780 views Asked by At

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.

2

There are 2 answers

4
code_monk On BEST ANSWER

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

1
YwH On

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