1

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.

dom1
  • 425
  • 1
  • 2
  • 19
  • Can you give commands or more detail on how you are adding the values to etcd? – Dharani Dhar Golladasari Mar 28 '23 at 10:48
  • Note the second error message is not a duplicate object; it spells out that the `name` is invalid because it contains an underscore. – David Maze Mar 28 '23 at 11:15
  • yes, i want a dynamic error message. let's say that the restaurant name is invalid i will return 'restaurant name is invalid, it should contain only 'a-z' and numbers and '-'. But i want this error message to be dynamic that works with other controllers – dom1 Mar 28 '23 at 13:19
  • How is the user using your software? Via command line or a web frontend or app? It is usually best practice to let the presentation layer translate error codes into user-friendly messages. – Falco Mar 31 '23 at 10:18
  • yes, the user use a web frontend, and the backend with golang talk to k8s. I want to reformulate the error response that i get from k8s and send it to the frontend. – dom1 Mar 31 '23 at 10:27

2 Answers2

1

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

code_monk
  • 9,451
  • 2
  • 42
  • 41
  • Thank you for your answer, the approach of switch case helped me, but it didn't solve my entire problem. – dom1 Apr 04 '23 at 22:31
  • 1
    I see. Are you perhaps looking for the [exact regex](https://goplay.tools/snippet/bcZO7wa8Vnl) that would extract the desired text? – code_monk Apr 05 '23 at 17:34
  • exactly this is what i want, please add the solution `exact regex` to your answer to mark it as accepted answer. Thank you – dom1 Apr 05 '23 at 19:39
  • @dom1 awesome! glad it was helpful. I added the regex to the solution as suggested – code_monk Apr 06 '23 at 14:42
1

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 ""
YwH
  • 1,050
  • 5
  • 11
  • Thank you for your answer, the information err.Error() helped me, but it didn't solve my entire problem – dom1 Apr 04 '23 at 22:28