mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 06:16:08 -04:00
Rename coordinator to bootstrapper and rename roles
This commit is contained in:
parent
3280ed200c
commit
916e5d6b55
191 changed files with 1763 additions and 2030 deletions
|
@ -0,0 +1,90 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const fieldManager = "constellation-bootstrapper"
|
||||
|
||||
// Client implements k8sapi.Client interface and talks to the Kubernetes API.
|
||||
type Client struct {
|
||||
clientset kubernetes.Interface
|
||||
builder *resource.Builder
|
||||
}
|
||||
|
||||
// New creates a new Client, talking to the real k8s API.
|
||||
func New(config []byte) (*Client, error) {
|
||||
clientConfig, err := clientcmd.RESTConfigFromKubeConfig(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating k8s client config from kubeconfig: %w", err)
|
||||
}
|
||||
clientset, err := kubernetes.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating k8s client from kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
restClientGetter, err := newRESTClientGetter(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating k8s RESTClientGetter from kubeconfig: %w", err)
|
||||
}
|
||||
builder := resource.NewBuilder(restClientGetter).Unstructured()
|
||||
|
||||
return &Client{clientset: clientset, builder: builder}, nil
|
||||
}
|
||||
|
||||
// ApplyOneObject uses server-side apply to send unstructured JSON blobs to the server and let it handle the core logic.
|
||||
func (c *Client) ApplyOneObject(info *resource.Info, forceConflicts bool) error {
|
||||
// helper can be used to patch k8s resources using server-side-apply.
|
||||
helper := resource.NewHelper(info.Client, info.Mapping).
|
||||
WithFieldManager(fieldManager)
|
||||
|
||||
// server-side-apply uses unstructured JSON instead of strict typing on the client side.
|
||||
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing resource for server-side apply: encoding of resource: %w", err)
|
||||
}
|
||||
options := metav1.PatchOptions{
|
||||
Force: &forceConflicts,
|
||||
}
|
||||
obj, err := helper.Patch(
|
||||
info.Namespace,
|
||||
info.Name,
|
||||
types.ApplyPatchType,
|
||||
data,
|
||||
&options,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("applying object %v using server-side apply: %w", info, err)
|
||||
}
|
||||
|
||||
return info.Refresh(obj, true)
|
||||
}
|
||||
|
||||
// GetObjects tries to marshal the resources into []*resource.Info using a resource.Builder.
|
||||
func (c *Client) GetObjects(resources resources.Marshaler) ([]*resource.Info, error) {
|
||||
// convert our resource struct into YAML
|
||||
data, err := resources.Marshal()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting resources to YAML: %w", err)
|
||||
}
|
||||
// read into resource.Info using builder
|
||||
reader := bytes.NewReader(data)
|
||||
result := c.builder.
|
||||
ContinueOnError().
|
||||
NamespaceParam("default").
|
||||
DefaultNamespace().
|
||||
Stream(reader, "yaml").
|
||||
Flatten().
|
||||
Do()
|
||||
return result.Infos()
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
"google.golang.org/protobuf/proto"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
restfake "k8s.io/client-go/rest/fake"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
var (
|
||||
corev1GV = schema.GroupVersion{Version: "v1"}
|
||||
nginxDeployment = &apps.Deployment{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "nginx",
|
||||
},
|
||||
Name: "my-nginx",
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: proto.Int32(3),
|
||||
Selector: &v1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "nginx",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "nginx",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
Image: "nginx:1.14.2",
|
||||
Ports: []k8s.ContainerPort{
|
||||
{
|
||||
ContainerPort: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
nginxDeplJSON, _ = marshalJSON(nginxDeployment)
|
||||
nginxDeplYAML, _ = marshalYAML(nginxDeployment)
|
||||
)
|
||||
|
||||
type unmarshableResource struct{}
|
||||
|
||||
func (*unmarshableResource) Marshal() ([]byte, error) {
|
||||
return nil, errors.New("someErr")
|
||||
}
|
||||
|
||||
func stringBody(body string) io.ReadCloser {
|
||||
return io.NopCloser(bytes.NewReader([]byte(body)))
|
||||
}
|
||||
|
||||
func fakeClientWith(t *testing.T, testName string, data map[string]string) resource.FakeClientFunc {
|
||||
return func(version schema.GroupVersion) (resource.RESTClient, error) {
|
||||
return &restfake.RESTClient{
|
||||
GroupVersion: corev1GV,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Client: restfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
p := req.URL.Path
|
||||
q := req.URL.RawQuery
|
||||
if len(q) != 0 {
|
||||
p = p + "?" + q
|
||||
}
|
||||
body, ok := data[p]
|
||||
if !ok {
|
||||
t.Fatalf("%s: unexpected request: %s (%s)\n%#v", testName, p, req.URL, req)
|
||||
}
|
||||
header := http.Header{}
|
||||
header.Set("Content-Type", runtime.ContentTypeJSON)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: header,
|
||||
Body: stringBody(body),
|
||||
}, nil
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newClientWithFakes(t *testing.T, data map[string]string, objects ...runtime.Object) Client {
|
||||
clientset := fake.NewSimpleClientset(objects...)
|
||||
builder := resource.NewFakeBuilder(
|
||||
fakeClientWith(t, "", data),
|
||||
func() (meta.RESTMapper, error) {
|
||||
return testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), nil
|
||||
},
|
||||
func() (restmapper.CategoryExpander, error) {
|
||||
return resource.FakeCategoryExpander, nil
|
||||
}).
|
||||
Unstructured()
|
||||
client := Client{
|
||||
clientset: clientset,
|
||||
builder: builder,
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func failingClient() resource.FakeClientFunc {
|
||||
return func(version schema.GroupVersion) (resource.RESTClient, error) {
|
||||
return &restfake.RESTClient{
|
||||
GroupVersion: corev1GV,
|
||||
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||
Resp: &http.Response{StatusCode: 501},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newFailingClient(objects ...runtime.Object) Client {
|
||||
clientset := fake.NewSimpleClientset(objects...)
|
||||
builder := resource.NewFakeBuilder(
|
||||
failingClient(),
|
||||
func() (meta.RESTMapper, error) {
|
||||
return testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), nil
|
||||
},
|
||||
func() (restmapper.CategoryExpander, error) {
|
||||
return resource.FakeCategoryExpander, nil
|
||||
}).
|
||||
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
client := Client{
|
||||
clientset: clientset,
|
||||
builder: builder,
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func marshalJSON(obj runtime.Object) ([]byte, error) {
|
||||
serializer := json.NewSerializer(json.DefaultMetaFactory, nil, nil, false)
|
||||
var buf bytes.Buffer
|
||||
if err := serializer.Encode(obj, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func marshalYAML(obj runtime.Object) ([]byte, error) {
|
||||
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil)
|
||||
var buf bytes.Buffer
|
||||
if err := serializer.Encode(obj, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func TestApplyOneObject(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
httpResponseData map[string]string
|
||||
wantObj runtime.Object
|
||||
resourcesYAML string
|
||||
failingClient bool
|
||||
wantErr bool
|
||||
}{
|
||||
"apply works": {
|
||||
httpResponseData: map[string]string{
|
||||
"/deployments/my-nginx?fieldManager=constellation-bootstrapper&force=true": string(nginxDeplJSON),
|
||||
},
|
||||
wantObj: nginxDeployment,
|
||||
resourcesYAML: string(nginxDeplYAML),
|
||||
wantErr: false,
|
||||
},
|
||||
"apply fails": {
|
||||
httpResponseData: map[string]string{},
|
||||
wantObj: nginxDeployment,
|
||||
resourcesYAML: string(nginxDeplYAML),
|
||||
failingClient: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
var client Client
|
||||
if tc.failingClient {
|
||||
client = newFailingClient(tc.wantObj)
|
||||
} else {
|
||||
client = newClientWithFakes(t, tc.httpResponseData, tc.wantObj)
|
||||
}
|
||||
|
||||
reader := bytes.NewReader([]byte(tc.resourcesYAML))
|
||||
res := client.builder.
|
||||
ContinueOnError().
|
||||
Stream(reader, "yaml").
|
||||
Flatten().
|
||||
Do()
|
||||
assert.NoError(res.Err())
|
||||
infos, err := res.Infos()
|
||||
assert.NoError(err)
|
||||
require.Len(infos, 1)
|
||||
|
||||
err = client.ApplyOneObject(infos[0], true)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetObjects(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
wantResources resources.Marshaler
|
||||
httpResponseData map[string]string
|
||||
resourcesYAML string
|
||||
wantErr bool
|
||||
}{
|
||||
"GetObjects works on cluster-autoscaler deployment": {
|
||||
wantResources: resources.NewDefaultAutoscalerDeployment(nil, nil, nil),
|
||||
resourcesYAML: string(nginxDeplYAML),
|
||||
wantErr: false,
|
||||
},
|
||||
"GetObjects works on cloud-controller-manager deployment": {
|
||||
wantResources: resources.NewDefaultCloudControllerManagerDeployment("someProvider", "someImage", "somePath", "someCIDR", nil, nil, nil, nil),
|
||||
resourcesYAML: string(nginxDeplYAML),
|
||||
wantErr: false,
|
||||
},
|
||||
"GetObjects Marshal failure detected": {
|
||||
wantResources: &unmarshableResource{},
|
||||
resourcesYAML: string(nginxDeplYAML),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := newClientWithFakes(t, tc.httpResponseData)
|
||||
infos, err := client.GetObjects(tc.wantResources)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotNil(infos)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/discovery/cached/memory"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// restClientGetter implements k8s.io/cli-runtime/pkg/resource.RESTClientGetter.
|
||||
type restClientGetter struct {
|
||||
clientconfig clientcmd.ClientConfig
|
||||
}
|
||||
|
||||
// newRESTClientGetter creates a new restClientGetter using a kubeconfig.
|
||||
func newRESTClientGetter(kubeconfig []byte) (*restClientGetter, error) {
|
||||
clientconfig, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawconfig, err := clientconfig.RawConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientconfig = clientcmd.NewDefaultClientConfig(rawconfig, &clientcmd.ConfigOverrides{})
|
||||
|
||||
return &restClientGetter{clientconfig}, nil
|
||||
}
|
||||
|
||||
// ToRESTConfig returns k8s REST client config.
|
||||
func (r *restClientGetter) ToRESTConfig() (*rest.Config, error) {
|
||||
return r.clientconfig.ClientConfig()
|
||||
}
|
||||
|
||||
// ToDiscoveryClient creates new k8s discovery client from restClientGetter.
|
||||
func (r *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
restconfig, err := r.clientconfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dc, err := discovery.NewDiscoveryClientForConfig(restconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return memory.NewMemCacheClient(dc), nil
|
||||
}
|
||||
|
||||
// ToRESTMapper creates new k8s RESTMapper from restClientGetter.
|
||||
func (r *restClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
dc, err := r.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return restmapper.NewDeferredDiscoveryRESTMapper(dc), nil
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader returns the inner k8s ClientConfig.
|
||||
func (r *restClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return r.clientconfig
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
const testingKubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ""
|
||||
server: https://192.0.2.0:6443
|
||||
name: kubernetes
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kubernetes
|
||||
user: kubernetes-admin
|
||||
name: kubernetes-admin@kubernetes
|
||||
current-context: kubernetes-admin@kubernetes
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kubernetes-admin
|
||||
user:
|
||||
client-certificate-data: ""
|
||||
client-key-data: ""
|
||||
`
|
||||
|
||||
type stubClientConfig struct {
|
||||
RawConfigConfig clientcmdapi.Config
|
||||
RawConfigErr error
|
||||
ClientConfigConfig *restclient.Config
|
||||
ClientConfigErr error
|
||||
NamespaceString string
|
||||
NamespaceOverridden bool
|
||||
NamespaceErr error
|
||||
ConfigAccessResult clientcmd.ConfigAccess
|
||||
}
|
||||
|
||||
func (s *stubClientConfig) RawConfig() (clientcmdapi.Config, error) {
|
||||
return s.RawConfigConfig, s.RawConfigErr
|
||||
}
|
||||
|
||||
func (s *stubClientConfig) ClientConfig() (*restclient.Config, error) {
|
||||
return s.ClientConfigConfig, s.ClientConfigErr
|
||||
}
|
||||
|
||||
func (s *stubClientConfig) Namespace() (string, bool, error) {
|
||||
return s.NamespaceString, s.NamespaceOverridden, s.NamespaceErr
|
||||
}
|
||||
|
||||
func (s *stubClientConfig) ConfigAccess() clientcmd.ConfigAccess {
|
||||
return s.ConfigAccessResult
|
||||
}
|
||||
|
||||
func TestNewRESTClientGetter(t *testing.T) {
|
||||
require := require.New(t)
|
||||
result, err := newRESTClientGetter([]byte(testingKubeconfig))
|
||||
require.NoError(err)
|
||||
require.NotNil(result)
|
||||
}
|
||||
|
||||
func TestToRESTConfig(t *testing.T) {
|
||||
require := require.New(t)
|
||||
getter := restClientGetter{
|
||||
clientconfig: &stubClientConfig{
|
||||
ClientConfigConfig: &restclient.Config{},
|
||||
},
|
||||
}
|
||||
result, err := getter.ToRESTConfig()
|
||||
require.NoError(err)
|
||||
require.NotNil(result)
|
||||
}
|
||||
|
||||
func TestToDiscoveryClient(t *testing.T) {
|
||||
require := require.New(t)
|
||||
getter := restClientGetter{
|
||||
clientconfig: &stubClientConfig{
|
||||
ClientConfigConfig: &restclient.Config{},
|
||||
},
|
||||
}
|
||||
result, err := getter.ToDiscoveryClient()
|
||||
require.NoError(err)
|
||||
require.NotNil(result)
|
||||
}
|
||||
|
||||
func TestToDiscoveryClientFail(t *testing.T) {
|
||||
require := require.New(t)
|
||||
getter := restClientGetter{
|
||||
clientconfig: &stubClientConfig{
|
||||
ClientConfigErr: errors.New("someErr"),
|
||||
},
|
||||
}
|
||||
_, err := getter.ToDiscoveryClient()
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func TestToRESTMapper(t *testing.T) {
|
||||
require := require.New(t)
|
||||
getter := restClientGetter{
|
||||
clientconfig: &stubClientConfig{
|
||||
ClientConfigConfig: &restclient.Config{},
|
||||
},
|
||||
}
|
||||
result, err := getter.ToRESTMapper()
|
||||
require.NoError(err)
|
||||
require.NotNil(result)
|
||||
}
|
||||
|
||||
func TestToRESTMapperFail(t *testing.T) {
|
||||
require := require.New(t)
|
||||
getter := restClientGetter{
|
||||
clientconfig: &stubClientConfig{
|
||||
ClientConfigErr: errors.New("someErr"),
|
||||
},
|
||||
}
|
||||
_, err := getter.ToRESTMapper()
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func TestToRawKubeConfigLoader(t *testing.T) {
|
||||
clientConfig := stubClientConfig{
|
||||
ClientConfigConfig: &restclient.Config{},
|
||||
}
|
||||
require := require.New(t)
|
||||
getter := restClientGetter{
|
||||
clientconfig: &clientConfig,
|
||||
}
|
||||
result := getter.ToRawKubeConfigLoader()
|
||||
require.Equal(&clientConfig, result)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue