switch to go vendoring
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Kubernetes Registry Plugin for micro
|
||||
This is a plugin for go-micro that allows you to use Kubernetes as a registry.
|
||||
|
||||
|
||||
## Overview
|
||||
This registry plugin makes use of Annotations and Labels on a Kubernetes pod
|
||||
to build a service discovery mechanism.
|
||||
|
||||
|
||||
## RBAC
|
||||
If your Kubernetes cluster has RBAC enabled, a role and role binding
|
||||
will need to be created to allow this plugin to `list` and `patch` pods.
|
||||
|
||||
A cluster role can be used to specify the `list` and `patch`
|
||||
requirements, while a role binding per namespace can be used to apply
|
||||
the cluster role. The example RBAC configs below assume your Micro-based
|
||||
services are running in the `test` namespace, and the pods that contain
|
||||
the services are using the `micro-services` service account.
|
||||
|
||||
```
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: micro-registry
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- list
|
||||
- patch
|
||||
- watch
|
||||
```
|
||||
|
||||
```
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: micro-registry
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: micro-registry
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: micro-services
|
||||
namespace: test
|
||||
```
|
||||
|
||||
|
||||
## Gotchas
|
||||
* Registering/Deregistering relies on the HOSTNAME Environment Variable, which inside a pod
|
||||
is the place where it can be retrieved from. (This needs improving)
|
||||
|
||||
|
||||
## Connecting to the Kubernetes API
|
||||
### Within a pod
|
||||
If the `--registry_address` flag is omitted, the plugin will securely connect to
|
||||
the Kubernetes API using the pods "Service Account". No extra configuration is necessary.
|
||||
|
||||
Find out more about service accounts here. http://kubernetes.io/docs/user-guide/accessing-the-cluster/
|
||||
|
||||
### Outside of Kubernetes
|
||||
Some functions of the plugin should work, but its not been heavily tested.
|
||||
Currently no TLS support.
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
// Package api ...
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-micro/plugins/v4/registry/kubernetes/client/watch"
|
||||
)
|
||||
|
||||
// Request is used to construct a http request for the k8s API.
|
||||
type Request struct {
|
||||
client *http.Client
|
||||
header http.Header
|
||||
params url.Values
|
||||
method string
|
||||
host string
|
||||
namespace string
|
||||
|
||||
resource string
|
||||
resourceName *string
|
||||
body io.Reader
|
||||
|
||||
err error
|
||||
}
|
||||
|
||||
// Params is the object to pass in to set parameters
|
||||
// on a request.
|
||||
type Params struct {
|
||||
LabelSelector map[string]string
|
||||
Watch bool
|
||||
}
|
||||
|
||||
// Options ...
|
||||
type Options struct {
|
||||
Host string
|
||||
Namespace string
|
||||
BearerToken *string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// NewRequest creates a k8s api request.
|
||||
func NewRequest(opts *Options) *Request {
|
||||
req := Request{
|
||||
header: make(http.Header),
|
||||
params: make(url.Values),
|
||||
client: opts.Client,
|
||||
namespace: opts.Namespace,
|
||||
host: opts.Host,
|
||||
}
|
||||
|
||||
if opts.BearerToken != nil {
|
||||
req.SetHeader("Authorization", "Bearer "+*opts.BearerToken)
|
||||
}
|
||||
|
||||
return &req
|
||||
}
|
||||
|
||||
// verb sets method.
|
||||
func (r *Request) verb(method string) *Request {
|
||||
r.method = method
|
||||
return r
|
||||
}
|
||||
|
||||
// Get request.
|
||||
func (r *Request) Get() *Request {
|
||||
return r.verb("GET")
|
||||
}
|
||||
|
||||
// Post request.
|
||||
func (r *Request) Post() *Request {
|
||||
return r.verb("POST")
|
||||
}
|
||||
|
||||
// Put request.
|
||||
func (r *Request) Put() *Request {
|
||||
return r.verb("PUT")
|
||||
}
|
||||
|
||||
// Patch request
|
||||
// https://github.com/kubernetes/kubernetes/blob/master/docs/devel/api-conventions.md#patch-operations
|
||||
func (r *Request) Patch() *Request {
|
||||
return r.verb("PATCH").SetHeader("Content-Type", "application/strategic-merge-patch+json")
|
||||
}
|
||||
|
||||
// Delete request.
|
||||
func (r *Request) Delete() *Request {
|
||||
return r.verb("DELETE")
|
||||
}
|
||||
|
||||
// Namespace is to set the namespace to operate on.
|
||||
func (r *Request) Namespace(s string) *Request {
|
||||
r.namespace = s
|
||||
return r
|
||||
}
|
||||
|
||||
// Resource is the type of resource the operation is
|
||||
// for, such as "services", "endpoints" or "pods".
|
||||
func (r *Request) Resource(s string) *Request {
|
||||
r.resource = s
|
||||
return r
|
||||
}
|
||||
|
||||
// Name is for targeting a specific resource by id.
|
||||
func (r *Request) Name(s string) *Request {
|
||||
r.resourceName = &s
|
||||
return r
|
||||
}
|
||||
|
||||
// Body pass in a body to set, this is for POST, PUT
|
||||
// and PATCH requests.
|
||||
func (r *Request) Body(in interface{}) *Request {
|
||||
b := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(b).Encode(&in); err != nil {
|
||||
r.err = err
|
||||
return r
|
||||
}
|
||||
|
||||
r.body = b
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Params isused to set parameters on a request.
|
||||
func (r *Request) Params(p *Params) *Request {
|
||||
for k, v := range p.LabelSelector {
|
||||
// create new key=value pair
|
||||
value := fmt.Sprintf("%s=%s", k, v)
|
||||
// check if there's an existing value
|
||||
if label := r.params.Get("labelSelector"); len(label) > 0 {
|
||||
value = fmt.Sprintf("%s,%s", label, value)
|
||||
}
|
||||
// set and overwrite the value
|
||||
r.params.Set("labelSelector", value)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetHeader sets a header on a request with
|
||||
// a `key` and `value`.
|
||||
func (r *Request) SetHeader(key, value string) *Request {
|
||||
r.header.Add(key, value)
|
||||
return r
|
||||
}
|
||||
|
||||
// request builds the http.Request from the options.
|
||||
func (r *Request) request() (*http.Request, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/namespaces/%s/%s/", r.host, r.namespace, r.resource)
|
||||
|
||||
// append resourceName if it is present
|
||||
if r.resourceName != nil {
|
||||
url += *r.resourceName
|
||||
}
|
||||
|
||||
// append any query params
|
||||
if len(r.params) > 0 {
|
||||
url += "?" + r.params.Encode()
|
||||
}
|
||||
|
||||
// build request
|
||||
req, err := http.NewRequest(r.method, url, r.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set headers on request
|
||||
req.Header = r.header
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Do builds and triggers the request.
|
||||
func (r *Request) Do() *Response {
|
||||
if r.err != nil {
|
||||
return &Response{
|
||||
err: r.err,
|
||||
}
|
||||
}
|
||||
|
||||
req, err := r.request()
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// return res, err
|
||||
return newResponse(res, err)
|
||||
}
|
||||
|
||||
// Watch builds and triggers the request, but will watch instead of return
|
||||
// an object.
|
||||
func (r *Request) Watch() (watch.Watch, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
r.params.Set("watch", "true")
|
||||
|
||||
req, err := r.request()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := watch.NewBodyWatcher(req, r.client)
|
||||
|
||||
return w, err
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// Errors ...
|
||||
var (
|
||||
ErrNoPodName = errors.New("no pod name provided")
|
||||
ErrNotFound = errors.New("pod not found")
|
||||
ErrDecode = errors.New("error decoding")
|
||||
ErrOther = errors.New("unspecified error occurred in k8s registry")
|
||||
)
|
||||
|
||||
// Response ...
|
||||
type Response struct {
|
||||
res *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// Error returns an error.
|
||||
func (r *Response) Error() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// StatusCode returns status code for response.
|
||||
func (r *Response) StatusCode() int {
|
||||
return r.res.StatusCode
|
||||
}
|
||||
|
||||
// Decode decodes body into `data`.
|
||||
func (r *Response) Decode(data interface{}) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
|
||||
var err error
|
||||
defer func() {
|
||||
nerr := r.res.Body.Close()
|
||||
if err != nil {
|
||||
err = nerr
|
||||
}
|
||||
}()
|
||||
|
||||
decoder := json.NewDecoder(r.res.Body)
|
||||
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return errors.Wrap(ErrDecode, err.Error())
|
||||
}
|
||||
|
||||
return r.err
|
||||
}
|
||||
|
||||
func newResponse(r *http.Response, err error) *Response {
|
||||
resp := &Response{
|
||||
res: r,
|
||||
err: err,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
// Check if request is successful.
|
||||
s := resp.res.StatusCode
|
||||
if s == http.StatusOK || s == http.StatusCreated || s == http.StatusNoContent {
|
||||
return resp
|
||||
}
|
||||
|
||||
if resp.res.StatusCode == http.StatusNotFound {
|
||||
resp.err = ErrNotFound
|
||||
return resp
|
||||
}
|
||||
|
||||
log.Errorf("K8s: request failed with code %v", resp.res.StatusCode)
|
||||
|
||||
b, err := io.ReadAll(resp.res.Body)
|
||||
if err == nil {
|
||||
log.Errorf("K8s: request failed with body: %s", string(b))
|
||||
}
|
||||
|
||||
resp.err = ErrOther
|
||||
|
||||
return resp
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Package client is the kubernetes registry client.
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
|
||||
"github.com/go-micro/plugins/v4/registry/kubernetes/client/api"
|
||||
"github.com/go-micro/plugins/v4/registry/kubernetes/client/watch"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
|
||||
|
||||
// ErrReadNamespace error when failed to read namespace.
|
||||
ErrReadNamespace = errors.New("could not read namespace from service account secret")
|
||||
)
|
||||
|
||||
// Client ...
|
||||
type client struct {
|
||||
opts *api.Options
|
||||
}
|
||||
|
||||
// NewClientByHost sets up a client by host.
|
||||
func NewClientByHost(host string) Kubernetes {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
DisableCompression: true,
|
||||
}
|
||||
|
||||
c := &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
return &client{
|
||||
opts: &api.Options{
|
||||
Client: c,
|
||||
Host: host,
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientInCluster should work similarly to the official api
|
||||
// NewInClient by setting up a client configuration for use within
|
||||
// a k8s pod.
|
||||
func NewClientInCluster() Kubernetes {
|
||||
host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
|
||||
s, err := os.Stat(serviceAccountPath)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
if s == nil || !s.IsDir() {
|
||||
logger.Fatal(errors.New("no k8s service account found"))
|
||||
}
|
||||
|
||||
t, err := os.ReadFile(path.Join(serviceAccountPath, "token"))
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
token := string(t)
|
||||
|
||||
ns, err := detectNamespace()
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt"))
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: crt,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
DisableCompression: true,
|
||||
},
|
||||
}
|
||||
|
||||
return &client{
|
||||
opts: &api.Options{
|
||||
Client: c,
|
||||
Host: host,
|
||||
Namespace: ns,
|
||||
BearerToken: &token,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListPods ...
|
||||
func (c *client) ListPods(labels map[string]string) (*PodList, error) {
|
||||
var pods PodList
|
||||
err := api.NewRequest(c.opts).Get().Resource("pods").Params(&api.Params{LabelSelector: labels}).Do().Decode(&pods)
|
||||
|
||||
return &pods, err
|
||||
}
|
||||
|
||||
// UpdatePod ...
|
||||
func (c *client) UpdatePod(name string, p *Pod) (*Pod, error) {
|
||||
var pod Pod
|
||||
err := api.NewRequest(c.opts).Patch().Resource("pods").Name(name).Body(p).Do().Decode(&pod)
|
||||
|
||||
return &pod, err
|
||||
}
|
||||
|
||||
// WatchPods ...
|
||||
func (c *client) WatchPods(labels map[string]string) (watch.Watch, error) {
|
||||
return api.NewRequest(c.opts).Get().Resource("pods").Params(&api.Params{LabelSelector: labels}).Watch()
|
||||
}
|
||||
|
||||
func detectNamespace() (string, error) {
|
||||
nsPath := path.Join(serviceAccountPath, "namespace")
|
||||
|
||||
// Make sure it's a file and we can read it
|
||||
if s, err := os.Stat(nsPath); err != nil {
|
||||
return "", err
|
||||
} else if s.IsDir() {
|
||||
return "", ErrReadNamespace
|
||||
}
|
||||
|
||||
// Read the file, and cast to a string
|
||||
ns, err := os.ReadFile(path.Clean(nsPath))
|
||||
if err != nil {
|
||||
return string(ns), err
|
||||
}
|
||||
|
||||
return string(ns), nil
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package client
|
||||
|
||||
import "github.com/go-micro/plugins/v4/registry/kubernetes/client/watch"
|
||||
|
||||
// Kubernetes ...
|
||||
type Kubernetes interface {
|
||||
ListPods(labels map[string]string) (*PodList, error)
|
||||
UpdatePod(podName string, pod *Pod) (*Pod, error)
|
||||
WatchPods(labels map[string]string) (watch.Watch, error)
|
||||
}
|
||||
|
||||
// PodList ...
|
||||
type PodList struct {
|
||||
Items []Pod `json:"items"`
|
||||
}
|
||||
|
||||
// Pod is the top level item for a pod.
|
||||
type Pod struct {
|
||||
Metadata *Meta `json:"metadata"`
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
// Meta ...
|
||||
type Meta struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Labels map[string]*string `json:"labels,omitempty"`
|
||||
Annotations map[string]*string `json:"annotations,omitempty"`
|
||||
DeletionTimestamp string `json:"deletionTimestamp,omitempty"`
|
||||
}
|
||||
|
||||
// Status ...
|
||||
type Status struct {
|
||||
PodIP string `json:"podIP"`
|
||||
Phase string `json:"phase"`
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// COPIED FROM
|
||||
// https://github.com/kubernetes/kubernetes/blob/7a725418af4661067b56506faabc2d44c6d7703a/pkg/util/crypto/crypto.go
|
||||
|
||||
// CertPoolFromFile returns an x509.CertPool containing the certificates in
|
||||
// the given PEM-encoded file. Returns an error if the file could not be read,
|
||||
// a certificate could not be parsed, or if the file does not contain any certificates.
|
||||
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
|
||||
certs, err := certificatesFromFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
for _, cert := range certs {
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// certificatesFromFile returns the x509.Certificates contained in the given
|
||||
// PEM-encoded file. Returns an error if the file could not be read, a
|
||||
// certificate could not be parsed, or if the file does not contain any certificates.
|
||||
func certificatesFromFile(file string) ([]*x509.Certificate, error) {
|
||||
if len(file) == 0 {
|
||||
return nil, errors.New("error reading certificates from an empty filename")
|
||||
}
|
||||
|
||||
pemBlock, err := os.ReadFile(filepath.Clean(file))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certs, err := CertsFromPEM(pemBlock)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading %s", file)
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// CertsFromPEM returns the x509.Certificates contained in the given PEM-encoded
|
||||
// byte array. Returns an error if a certificate could not be parsed, or if the
|
||||
// data does not contain any certificates.
|
||||
func CertsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) {
|
||||
ok := false
|
||||
certs := []*x509.Certificate{}
|
||||
|
||||
for len(pemCerts) > 0 {
|
||||
var block *pem.Block
|
||||
block, pemCerts = pem.Decode(pemCerts)
|
||||
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Only use PEM "CERTIFICATE" blocks without extra headers
|
||||
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return certs, err
|
||||
}
|
||||
|
||||
certs = append(certs, cert)
|
||||
ok = true
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return certs, errors.New("could not read any certificates")
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// bodyWatcher scans the body of a request for chunks.
|
||||
type bodyWatcher struct {
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
results chan Event
|
||||
res *http.Response
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
// Changes returns the results channel.
|
||||
func (wr *bodyWatcher) ResultChan() <-chan Event {
|
||||
return wr.results
|
||||
}
|
||||
|
||||
// Stop cancels the request.
|
||||
func (wr *bodyWatcher) Stop() {
|
||||
select {
|
||||
case <-wr.ctx.Done():
|
||||
return
|
||||
default:
|
||||
wr.stop()
|
||||
close(wr.results)
|
||||
}
|
||||
}
|
||||
|
||||
func (wr *bodyWatcher) stream() {
|
||||
reader := bufio.NewReader(wr.res.Body)
|
||||
|
||||
// ignore first few messages from stream,
|
||||
// as they are usually old.
|
||||
var ignore atomic.Bool
|
||||
|
||||
go func() {
|
||||
<-time.After(time.Second)
|
||||
ignore.Store(false)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
//nolint:errcheck
|
||||
defer wr.res.Body.Close()
|
||||
|
||||
for {
|
||||
// Read a line
|
||||
b, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Ignore for the first second
|
||||
if ignore.Load() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Send the event
|
||||
var event Event
|
||||
if err := json.Unmarshal(b, &event); err != nil {
|
||||
continue
|
||||
}
|
||||
wr.results <- event
|
||||
}
|
||||
|
||||
// stop the watcher
|
||||
wr.Stop()
|
||||
}()
|
||||
}
|
||||
|
||||
// NewBodyWatcher creates a k8s body watcher for a given http request.
|
||||
func NewBodyWatcher(req *http.Request, client *http.Client) (Watch, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
//nolint:bodyclose
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, errors.Wrap(err, "body watcher failed to make http request")
|
||||
}
|
||||
|
||||
wr := &bodyWatcher{
|
||||
ctx: ctx,
|
||||
results: make(chan Event),
|
||||
stop: cancel,
|
||||
req: req,
|
||||
res: res,
|
||||
}
|
||||
|
||||
go wr.stream()
|
||||
|
||||
return wr, nil
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Package watch implements the k8s watcher.
|
||||
package watch
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Watch ...
|
||||
type Watch interface {
|
||||
Stop()
|
||||
ResultChan() <-chan Event
|
||||
}
|
||||
|
||||
// EventType defines the possible types of events.
|
||||
type EventType string
|
||||
|
||||
// EventTypes used.
|
||||
const (
|
||||
Added EventType = "ADDED"
|
||||
Modified EventType = "MODIFIED"
|
||||
Deleted EventType = "DELETED"
|
||||
Error EventType = "ERROR"
|
||||
)
|
||||
|
||||
// Event represents a single event to a watched resource.
|
||||
type Event struct {
|
||||
Type EventType `json:"type"`
|
||||
Object json.RawMessage `json:"object"`
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
// Package kubernetes provides a kubernetes registry
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/util/cmd"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/go-micro/plugins/v4/registry/kubernetes/client"
|
||||
)
|
||||
|
||||
type kregistry struct {
|
||||
client client.Kubernetes
|
||||
timeout time.Duration
|
||||
options registry.Options
|
||||
}
|
||||
|
||||
var (
|
||||
// used on pods as labels & services to select
|
||||
// eg: svcSelectorPrefix+"svc.name"
|
||||
svcSelectorPrefix = "micro.mu/selector-"
|
||||
svcSelectorValue = "service"
|
||||
|
||||
labelTypeKey = "micro.mu/type"
|
||||
labelTypeValueService = "service"
|
||||
|
||||
// used on k8s services to scope a serialized
|
||||
// micro service by pod name.
|
||||
annotationServiceKeyPrefix = "micro.mu/service-"
|
||||
|
||||
// Pod status.
|
||||
podRunning = "Running"
|
||||
|
||||
// label name regex.
|
||||
labelRe = regexp.MustCompilePOSIX("[-A-Za-z0-9_.]")
|
||||
)
|
||||
|
||||
// Err are all package errors.
|
||||
var (
|
||||
ErrNoHostname = errors.New("failed to get podname from HOSTNAME variable")
|
||||
ErrNoNodesFound = errors.New("you must provide at least one node")
|
||||
)
|
||||
|
||||
// podSelector.
|
||||
var podSelector = map[string]string{
|
||||
labelTypeKey: labelTypeValueService,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultRegistries["kubernetes"] = NewRegistry
|
||||
}
|
||||
|
||||
func configure(k *kregistry, opts ...registry.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&k.options)
|
||||
}
|
||||
|
||||
// get first host
|
||||
var host string
|
||||
if len(k.options.Addrs) > 0 && len(k.options.Addrs[0]) > 0 {
|
||||
host = k.options.Addrs[0]
|
||||
}
|
||||
|
||||
if k.options.Timeout == 0 {
|
||||
k.options.Timeout = time.Second * 1
|
||||
}
|
||||
|
||||
// if no hosts setup, assume InCluster
|
||||
var c client.Kubernetes
|
||||
if len(host) == 0 {
|
||||
c = client.NewClientInCluster()
|
||||
} else {
|
||||
c = client.NewClientByHost(host)
|
||||
}
|
||||
|
||||
k.client = c
|
||||
k.timeout = k.options.Timeout
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serviceName generates a valid service name for k8s labels.
|
||||
func serviceName(name string) string {
|
||||
aname := make([]byte, len(name))
|
||||
|
||||
for i, r := range []byte(name) {
|
||||
if !labelRe.Match([]byte{r}) {
|
||||
aname[i] = '_'
|
||||
continue
|
||||
}
|
||||
|
||||
aname[i] = r
|
||||
}
|
||||
|
||||
return string(aname)
|
||||
}
|
||||
|
||||
// Init allows reconfig of options.
|
||||
func (c *kregistry) Init(opts ...registry.Option) error {
|
||||
return configure(c, opts...)
|
||||
}
|
||||
|
||||
// Options returns the registry Options.
|
||||
func (c *kregistry) Options() registry.Options {
|
||||
return c.options
|
||||
}
|
||||
|
||||
// Register sets a service selector label and an annotation with a
|
||||
// serialized version of the service passed in.
|
||||
func (c *kregistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
|
||||
if len(s.Nodes) == 0 {
|
||||
return ErrNoNodesFound
|
||||
}
|
||||
|
||||
svcName := s.Name
|
||||
|
||||
// TODO: grab podname from somewhere better than this.
|
||||
podName, err := getPodName()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to register")
|
||||
}
|
||||
|
||||
// encode micro service
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc := string(b)
|
||||
|
||||
pod := &client.Pod{
|
||||
Metadata: &client.Meta{
|
||||
Labels: map[string]*string{
|
||||
labelTypeKey: &labelTypeValueService,
|
||||
svcSelectorPrefix + serviceName(svcName): &svcSelectorValue,
|
||||
},
|
||||
Annotations: map[string]*string{
|
||||
annotationServiceKeyPrefix + serviceName(svcName): &svc,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := c.client.UpdatePod(podName, pod); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deregister nils out any things set in Register.
|
||||
func (c *kregistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error {
|
||||
if len(s.Nodes) == 0 {
|
||||
return ErrNoNodesFound
|
||||
}
|
||||
|
||||
svcName := s.Name
|
||||
|
||||
// TODO: grab podname from somewhere better than env var.
|
||||
podName, err := getPodName()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to deregister")
|
||||
}
|
||||
|
||||
pod := &client.Pod{
|
||||
Metadata: &client.Meta{
|
||||
Labels: map[string]*string{
|
||||
svcSelectorPrefix + serviceName(svcName): nil,
|
||||
},
|
||||
Annotations: map[string]*string{
|
||||
annotationServiceKeyPrefix + serviceName(svcName): nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := c.client.UpdatePod(podName, pod); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetService will get all the pods with the given service selector,
|
||||
// and build services from the annotations.
|
||||
func (c *kregistry) GetService(name string, opts ...registry.GetOption) ([]*registry.Service, error) {
|
||||
pods, err := c.client.ListPods(map[string]string{
|
||||
svcSelectorPrefix + serviceName(name): svcSelectorValue,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pods.Items) == 0 {
|
||||
return nil, registry.ErrNotFound
|
||||
}
|
||||
|
||||
// svcs mapped by version
|
||||
svcs := make(map[string]*registry.Service)
|
||||
|
||||
// loop through items
|
||||
for _, pod := range pods.Items {
|
||||
if pod.Status.Phase != podRunning || pod.Metadata.DeletionTimestamp != "" {
|
||||
continue
|
||||
}
|
||||
// get serialized service from annotation
|
||||
svcStr, ok := pod.Metadata.Annotations[annotationServiceKeyPrefix+serviceName(name)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var svc registry.Service
|
||||
|
||||
// unmarshal service string
|
||||
err := json.Unmarshal([]byte(*svcStr), &svc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal service '%s' from pod annotation", name)
|
||||
}
|
||||
|
||||
// merge up pod service & ip with versioned service.
|
||||
vs, ok := svcs[svc.Version]
|
||||
if !ok {
|
||||
svcs[svc.Version] = &svc
|
||||
continue
|
||||
}
|
||||
|
||||
vs.Nodes = append(vs.Nodes, svc.Nodes...)
|
||||
}
|
||||
|
||||
list := make([]*registry.Service, 0, len(svcs))
|
||||
for _, val := range svcs {
|
||||
list = append(list, val)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// ListServices will list all the service names.
|
||||
func (c *kregistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) {
|
||||
pods, err := c.client.ListPods(podSelector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// svcs mapped by name+version
|
||||
svcs := make(map[string]*registry.Service)
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
if pod.Status.Phase != podRunning || pod.Metadata.DeletionTimestamp != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range pod.Metadata.Annotations {
|
||||
if !strings.HasPrefix(k, annotationServiceKeyPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
// we have to unmarshal the annotation itself since the
|
||||
// key is encoded to match the regex restriction.
|
||||
var svc registry.Service
|
||||
if err := json.Unmarshal([]byte(*v), &svc); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s, ok := svcs[svc.Name+svc.Version]
|
||||
if !ok {
|
||||
svcs[svc.Name+svc.Version] = &svc
|
||||
continue
|
||||
}
|
||||
|
||||
// append to service:version nodes
|
||||
s.Nodes = append(s.Nodes, svc.Nodes...)
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
list := make([]*registry.Service, len(svcs))
|
||||
|
||||
for _, s := range svcs {
|
||||
list[i] = s
|
||||
i++
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Watch returns a kubernetes watcher.
|
||||
func (c *kregistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
|
||||
return newWatcher(c, opts...)
|
||||
}
|
||||
|
||||
func (c *kregistry) String() string {
|
||||
return "kubernetes"
|
||||
}
|
||||
|
||||
// NewRegistry creates a kubernetes registry.
|
||||
func NewRegistry(opts ...registry.Option) registry.Registry {
|
||||
k := &kregistry{
|
||||
options: registry.Options{},
|
||||
}
|
||||
|
||||
//nolint:errcheck,gosec
|
||||
configure(k, opts...)
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
func getPodName() (string, error) {
|
||||
podName := os.Getenv("HOSTNAME")
|
||||
if len(podName) == 0 {
|
||||
return "", ErrNoHostname
|
||||
}
|
||||
|
||||
return podName, nil
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
|
||||
"github.com/go-micro/plugins/v4/registry/kubernetes/client"
|
||||
"github.com/go-micro/plugins/v4/registry/kubernetes/client/watch"
|
||||
)
|
||||
|
||||
var (
|
||||
deleteAction = "delete"
|
||||
)
|
||||
|
||||
type k8sWatcher struct {
|
||||
registry *kregistry
|
||||
watcher watch.Watch
|
||||
next chan *registry.Result
|
||||
|
||||
sync.RWMutex
|
||||
pods map[string]*client.Pod
|
||||
}
|
||||
|
||||
// build a cache of pods when the watcher starts.
|
||||
func (k *k8sWatcher) updateCache() ([]*registry.Result, error) {
|
||||
podList, err := k.registry.client.ListPods(podSelector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []*registry.Result
|
||||
|
||||
for _, p := range podList.Items {
|
||||
// Copy to new var as p gets overwritten by the loop
|
||||
pod := p
|
||||
rslts := k.buildPodResults(&pod, nil)
|
||||
results = append(results, rslts...)
|
||||
|
||||
k.Lock()
|
||||
k.pods[pod.Metadata.Name] = &pod
|
||||
k.Unlock()
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// look through pod annotations, compare against cache if present
|
||||
// and return a list of results to send down the wire.
|
||||
func (k *k8sWatcher) buildPodResults(pod *client.Pod, cache *client.Pod) []*registry.Result {
|
||||
var results []*registry.Result
|
||||
|
||||
ignore := make(map[string]bool)
|
||||
|
||||
if pod.Metadata != nil {
|
||||
results, ignore = podBuildResult(pod, cache)
|
||||
}
|
||||
|
||||
// loop through cache annotations to find services
|
||||
// not accounted for above, and "delete" them.
|
||||
if cache != nil && cache.Metadata != nil {
|
||||
for annKey, annVal := range cache.Metadata.Annotations {
|
||||
if ignore[annKey] {
|
||||
continue
|
||||
}
|
||||
|
||||
// check this annotation kv is a service notation
|
||||
if !strings.HasPrefix(annKey, annotationServiceKeyPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
rslt := ®istry.Result{Action: deleteAction}
|
||||
|
||||
// unmarshal service notation from annotation value
|
||||
if err := json.Unmarshal([]byte(*annVal), &rslt.Service); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, rslt)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// handleEvent will taken an event from the k8s pods API and do the correct
|
||||
// things with the result, based on the local cache.
|
||||
func (k *k8sWatcher) handleEvent(event watch.Event) {
|
||||
var pod client.Pod
|
||||
if err := json.Unmarshal([]byte(event.Object), &pod); err != nil {
|
||||
logger.Error("K8s Watcher: Couldnt unmarshal event object from pod")
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:exhaustive
|
||||
switch event.Type {
|
||||
// Pod was modified
|
||||
case watch.Modified:
|
||||
k.RLock()
|
||||
cache := k.pods[pod.Metadata.Name]
|
||||
k.RUnlock()
|
||||
|
||||
// service could have been added, edited or removed.
|
||||
var results []*registry.Result
|
||||
|
||||
if pod.Status.Phase == podRunning {
|
||||
results = k.buildPodResults(&pod, cache)
|
||||
} else {
|
||||
// passing in cache might not return all results
|
||||
results = k.buildPodResults(&pod, nil)
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
// pod isnt running
|
||||
if pod.Status.Phase != podRunning || pod.Metadata.DeletionTimestamp != "" {
|
||||
result.Action = deleteAction
|
||||
}
|
||||
k.next <- result
|
||||
}
|
||||
|
||||
k.Lock()
|
||||
k.pods[pod.Metadata.Name] = &pod
|
||||
k.Unlock()
|
||||
|
||||
return
|
||||
|
||||
// Pod was deleted
|
||||
// passing in cache might not return all results
|
||||
case watch.Deleted:
|
||||
results := k.buildPodResults(&pod, nil)
|
||||
|
||||
for _, result := range results {
|
||||
result.Action = deleteAction
|
||||
k.next <- result
|
||||
}
|
||||
|
||||
k.Lock()
|
||||
delete(k.pods, pod.Metadata.Name)
|
||||
k.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Next will block until a new result comes in.
|
||||
func (k *k8sWatcher) Next() (*registry.Result, error) {
|
||||
r, ok := <-k.next
|
||||
if !ok {
|
||||
return nil, errors.New("result chan closed")
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Stop will cancel any requests, and close channels.
|
||||
func (k *k8sWatcher) Stop() {
|
||||
k.watcher.Stop()
|
||||
|
||||
select {
|
||||
case <-k.next:
|
||||
return
|
||||
default:
|
||||
close(k.next)
|
||||
}
|
||||
}
|
||||
|
||||
func newWatcher(kr *kregistry, opts ...registry.WatchOption) (registry.Watcher, error) {
|
||||
var wo registry.WatchOptions
|
||||
for _, o := range opts {
|
||||
o(&wo)
|
||||
}
|
||||
|
||||
selector := podSelector
|
||||
if len(wo.Service) > 0 {
|
||||
selector = map[string]string{
|
||||
svcSelectorPrefix + serviceName(wo.Service): svcSelectorValue,
|
||||
}
|
||||
}
|
||||
|
||||
// Create watch request
|
||||
watcher, err := kr.client.WatchPods(selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := &k8sWatcher{
|
||||
registry: kr,
|
||||
watcher: watcher,
|
||||
next: make(chan *registry.Result),
|
||||
pods: make(map[string]*client.Pod),
|
||||
}
|
||||
|
||||
// update cache, but dont emit changes
|
||||
if _, err := k.updateCache(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// range over watch request changes, and invoke
|
||||
// the update event
|
||||
go func() {
|
||||
for event := range watcher.ResultChan() {
|
||||
k.handleEvent(event)
|
||||
}
|
||||
|
||||
k.Stop()
|
||||
}()
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func podBuildResult(pod *client.Pod, cache *client.Pod) ([]*registry.Result, map[string]bool) {
|
||||
results := make([]*registry.Result, 0, len(pod.Metadata.Annotations))
|
||||
ignore := make(map[string]bool)
|
||||
|
||||
for annKey, annVal := range pod.Metadata.Annotations {
|
||||
// check this annotation kv is a service notation
|
||||
if !strings.HasPrefix(annKey, annotationServiceKeyPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
if annVal == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore when we check the cached annotations
|
||||
// as we take care of it here
|
||||
ignore[annKey] = true
|
||||
|
||||
// compare against cache.
|
||||
var (
|
||||
cacheExists bool
|
||||
cav *string
|
||||
)
|
||||
|
||||
if cache != nil && cache.Metadata != nil {
|
||||
cav, cacheExists = cache.Metadata.Annotations[annKey]
|
||||
if cacheExists && cav != nil && cav == annVal {
|
||||
// service notation exists and is identical -
|
||||
// no change result required.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
rslt := ®istry.Result{}
|
||||
if cacheExists {
|
||||
rslt.Action = "update"
|
||||
} else {
|
||||
rslt.Action = "create"
|
||||
}
|
||||
|
||||
// unmarshal service notation from annotation value
|
||||
if err := json.Unmarshal([]byte(*annVal), &rslt.Service); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, rslt)
|
||||
}
|
||||
|
||||
return results, ignore
|
||||
}
|
||||
Reference in New Issue
Block a user