Skip to content

Commit 27d8acc

Browse files
authored
Readiness failover (#295)
* wip readiness failover implementation * don't spam the logs with err messages * use kubernetes api to update the failover label * add ca cert validation to client * manually read files * seperate k8s and failover code * improve err handling * use pointers * fix config parsing * initialize http client * use correct state * fix msgf arg * use Msg as no arguments are passed
1 parent 699902a commit 27d8acc

File tree

5 files changed

+420
-0
lines changed

5 files changed

+420
-0
lines changed

backend/k8sapi/client.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright © 2022 by PACE Telematics GmbH. All rights reserved.
2+
// Created at 2022/01/24 by Vincent Landgraf
3+
4+
package k8sapi
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"crypto/tls"
10+
"crypto/x509"
11+
"encoding/json"
12+
"fmt"
13+
"io"
14+
"net/http"
15+
"os"
16+
"strings"
17+
18+
"github.com/caarlos0/env"
19+
"github.com/pace/bricks/http/transport"
20+
"github.com/pace/bricks/maintenance/log"
21+
)
22+
23+
// Client minimal client for the kubernetes API
24+
type Client struct {
25+
Podname string
26+
Namespace string
27+
CACert []byte
28+
Token string
29+
cfg Config
30+
HttpClient *http.Client
31+
}
32+
33+
// NewClient create new api client
34+
func NewClient() (*Client, error) {
35+
cl := Client{
36+
HttpClient: &http.Client{},
37+
}
38+
39+
// lookup hostname (for pod update)
40+
hostname, err := os.Hostname()
41+
if err != nil {
42+
return nil, err
43+
}
44+
cl.Podname = hostname
45+
46+
// parse environment including secrets mounted by kubernetes
47+
err = env.Parse(&cl.cfg)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
caData, err := os.ReadFile(cl.cfg.CACertFile)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to read %q: %v", cl.cfg.CACertFile, err)
55+
}
56+
cl.CACert = []byte(strings.TrimSpace(string(caData)))
57+
58+
namespaceData, err := os.ReadFile(cl.cfg.NamespaceFile)
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to read %q: %v", cl.cfg.NamespaceFile, err)
61+
}
62+
cl.Namespace = strings.TrimSpace(string(namespaceData))
63+
64+
tokenData, err := os.ReadFile(cl.cfg.TokenFile)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to read %q: %v", cl.cfg.CACertFile, err)
67+
}
68+
cl.Token = strings.TrimSpace(string(tokenData))
69+
70+
// add kubernetes api server cert
71+
chain := transport.NewDefaultTransportChain()
72+
pool := x509.NewCertPool()
73+
ok := pool.AppendCertsFromPEM(cl.CACert)
74+
if !ok {
75+
return nil, fmt.Errorf("failed to load kubernetes ca cert")
76+
}
77+
chain.Final(&http.Transport{
78+
TLSClientConfig: &tls.Config{
79+
RootCAs: pool,
80+
},
81+
})
82+
cl.HttpClient.Transport = chain
83+
84+
return &cl, nil
85+
}
86+
87+
// SimpleRequest send a simple http request to kubernetes with the passed
88+
// method, url and requestObj, decoding the result into responseObj
89+
func (c *Client) SimpleRequest(ctx context.Context, method, url string, requestObj, responseObj interface{}) error {
90+
data, err := json.Marshal(requestObj)
91+
if err != nil {
92+
panic(err)
93+
}
94+
95+
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(data))
96+
if err != nil {
97+
panic(err)
98+
}
99+
100+
req.Header.Set("Content-Type", "application/json-patch+json")
101+
req.Header.Set("Authorization", "Bearer "+c.Token)
102+
103+
resp, err := c.HttpClient.Do(req)
104+
if err != nil {
105+
log.Ctx(ctx).Debug().Err(err).Msg("failed to do api request")
106+
return err
107+
}
108+
defer resp.Body.Close()
109+
110+
if resp.StatusCode > 299 {
111+
body, _ := io.ReadAll(resp.Body) // nolint: errcheck
112+
log.Ctx(ctx).Debug().Msgf("failed to do api request, due to: %s", string(body))
113+
return fmt.Errorf("k8s request failed with %s", resp.Status)
114+
}
115+
116+
return json.NewDecoder(resp.Body).Decode(responseObj)
117+
}

backend/k8sapi/config.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright © 2022 by PACE Telematics GmbH. All rights reserved.
2+
// Created at 2022/01/24 by Vincent Landgraf
3+
4+
package k8sapi
5+
6+
// Config gathers the required kubernetes system configuration to use the
7+
// kubernetes API
8+
type Config struct {
9+
Host string `env:"KUBERNETES_SERVICE_HOST" envDefault:"localhost"`
10+
Port int `env:"KUBERNETES_PORT_443_TCP_PORT" envDefault:"433"`
11+
NamespaceFile string `env:"KUBERNETES_NAMESPACE_FILE" envDefault:"/run/secrets/kubernetes.io/serviceaccount/namespace"`
12+
CACertFile string `env:"KUBERNETES_API_CA_FILE" envDefault:"/run/secrets/kubernetes.io/serviceaccount/ca.crt"`
13+
TokenFile string `env:"KUBERNETES_API_TOKEN_FILE" envDefault:"/run/secrets/kubernetes.io/serviceaccount/token"`
14+
}

backend/k8sapi/pod.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright © 2022 by PACE Telematics GmbH. All rights reserved.
2+
// Created at 2022/01/24 by Vincent Landgraf
3+
4+
package k8sapi
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
)
11+
12+
// SetCurrentPodLabel set the label for the current pod in the current
13+
// namespace (requires patch on pods resource)
14+
func (c *Client) SetCurrentPodLabel(ctx context.Context, label, value string) error {
15+
return c.SetPodLabel(ctx, c.Namespace, c.Podname, label, value)
16+
}
17+
18+
// SetPodLabel sets the label and value for the pod of the given namespace
19+
// (requires patch on pods resource in the given namespace)
20+
func (c *Client) SetPodLabel(ctx context.Context, namespace, podname, label, value string) error {
21+
pr := []struct {
22+
Op string `json:"op"`
23+
Path string `json:"path"`
24+
Value string `json:"value"`
25+
}{
26+
{
27+
Op: "add",
28+
Path: "/metadata/labels/" + label,
29+
Value: value,
30+
},
31+
}
32+
url := fmt.Sprintf("https://%s:%d/api/v1/namespaces/%s/pods/%s",
33+
c.cfg.Host, c.cfg.Port, namespace, podname)
34+
var resp interface{}
35+
36+
return c.SimpleRequest(ctx, http.MethodPatch, url, &pr, &resp)
37+
}

0 commit comments

Comments
 (0)