mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-12-09 21:16:52 -05: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
19
bootstrapper/README.md
Normal file
19
bootstrapper/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Bootstrapper
|
||||
|
||||
## Naming convention
|
||||
|
||||
### Network
|
||||
|
||||
IP addresses:
|
||||
|
||||
* ip: numeric IP address
|
||||
* host: either IP address or hostname
|
||||
* endpoint: host+port
|
||||
|
||||
### Keys
|
||||
|
||||
Kinds:
|
||||
|
||||
* key: symmetric key
|
||||
* pubKey: public key
|
||||
* privKey: private key
|
||||
88
bootstrapper/cloudprovider/azure/api.go
Normal file
88
bootstrapper/cloudprovider/azure/api.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
)
|
||||
|
||||
type imdsAPI interface {
|
||||
Retrieve(ctx context.Context) (metadataResponse, error)
|
||||
}
|
||||
|
||||
type virtualNetworksClientListPager interface {
|
||||
NextPage(ctx context.Context) bool
|
||||
PageResponse() armnetwork.VirtualNetworksClientListResponse
|
||||
}
|
||||
|
||||
type virtualNetworksAPI interface {
|
||||
List(resourceGroupName string, options *armnetwork.VirtualNetworksClientListOptions) virtualNetworksClientListPager
|
||||
}
|
||||
|
||||
type securityGroupsClientListPager interface {
|
||||
NextPage(ctx context.Context) bool
|
||||
PageResponse() armnetwork.SecurityGroupsClientListResponse
|
||||
}
|
||||
|
||||
type securityGroupsAPI interface {
|
||||
List(resourceGroupName string, options *armnetwork.SecurityGroupsClientListOptions) securityGroupsClientListPager
|
||||
}
|
||||
|
||||
type networkInterfacesAPI interface {
|
||||
GetVirtualMachineScaleSetNetworkInterface(ctx context.Context, resourceGroupName string,
|
||||
virtualMachineScaleSetName string, virtualmachineIndex string, networkInterfaceName string,
|
||||
options *armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceOptions,
|
||||
) (armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceResponse, error)
|
||||
Get(ctx context.Context, resourceGroupName string, networkInterfaceName string,
|
||||
options *armnetwork.InterfacesClientGetOptions) (armnetwork.InterfacesClientGetResponse, error)
|
||||
}
|
||||
|
||||
type publicIPAddressesAPI interface {
|
||||
GetVirtualMachineScaleSetPublicIPAddress(ctx context.Context, resourceGroupName string,
|
||||
virtualMachineScaleSetName string, virtualmachineIndex string, networkInterfaceName string,
|
||||
ipConfigurationName string, publicIPAddressName string,
|
||||
options *armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressOptions,
|
||||
) (armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse, error)
|
||||
Get(ctx context.Context, resourceGroupName string, publicIPAddressName string,
|
||||
options *armnetwork.PublicIPAddressesClientGetOptions) (armnetwork.PublicIPAddressesClientGetResponse, error)
|
||||
}
|
||||
|
||||
type virtualMachineScaleSetVMsAPI interface {
|
||||
Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string, options *armcompute.VirtualMachineScaleSetVMsClientGetOptions) (armcompute.VirtualMachineScaleSetVMsClientGetResponse, error)
|
||||
List(resourceGroupName string, virtualMachineScaleSetName string, options *armcompute.VirtualMachineScaleSetVMsClientListOptions) virtualMachineScaleSetVMsClientListPager
|
||||
}
|
||||
|
||||
type virtualMachineScaleSetVMsClientListPager interface {
|
||||
NextPage(ctx context.Context) bool
|
||||
PageResponse() armcompute.VirtualMachineScaleSetVMsClientListResponse
|
||||
}
|
||||
|
||||
type scaleSetsAPI interface {
|
||||
List(resourceGroupName string, options *armcompute.VirtualMachineScaleSetsClientListOptions) virtualMachineScaleSetsClientListPager
|
||||
}
|
||||
|
||||
type loadBalancersClientListPager interface {
|
||||
NextPage(ctx context.Context) bool
|
||||
PageResponse() armnetwork.LoadBalancersClientListResponse
|
||||
}
|
||||
|
||||
type loadBalancerAPI interface {
|
||||
List(resourceGroupName string, options *armnetwork.LoadBalancersClientListOptions) loadBalancersClientListPager
|
||||
}
|
||||
|
||||
type virtualMachineScaleSetsClientListPager interface {
|
||||
NextPage(ctx context.Context) bool
|
||||
PageResponse() armcompute.VirtualMachineScaleSetsClientListResponse
|
||||
}
|
||||
|
||||
type tagsAPI interface {
|
||||
CreateOrUpdateAtScope(ctx context.Context, scope string, parameters armresources.TagsResource, options *armresources.TagsClientCreateOrUpdateAtScopeOptions) (armresources.TagsClientCreateOrUpdateAtScopeResponse, error)
|
||||
UpdateAtScope(ctx context.Context, scope string, parameters armresources.TagsPatchResource, options *armresources.TagsClientUpdateAtScopeOptions) (armresources.TagsClientUpdateAtScopeResponse, error)
|
||||
}
|
||||
|
||||
type applicationInsightsAPI interface {
|
||||
Get(ctx context.Context, resourceGroupName string, resourceName string, options *armapplicationinsights.ComponentsClientGetOptions) (armapplicationinsights.ComponentsClientGetResponse, error)
|
||||
}
|
||||
260
bootstrapper/cloudprovider/azure/api_test.go
Normal file
260
bootstrapper/cloudprovider/azure/api_test.go
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m,
|
||||
// https://github.com/census-instrumentation/opencensus-go/issues/1262
|
||||
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
|
||||
)
|
||||
}
|
||||
|
||||
type stubIMDSAPI struct {
|
||||
res metadataResponse
|
||||
retrieveErr error
|
||||
}
|
||||
|
||||
func (a *stubIMDSAPI) Retrieve(ctx context.Context) (metadataResponse, error) {
|
||||
return a.res, a.retrieveErr
|
||||
}
|
||||
|
||||
type stubNetworkInterfacesAPI struct {
|
||||
getInterface armnetwork.Interface
|
||||
getErr error
|
||||
}
|
||||
|
||||
func (a *stubNetworkInterfacesAPI) GetVirtualMachineScaleSetNetworkInterface(ctx context.Context, resourceGroupName string,
|
||||
virtualMachineScaleSetName string, virtualmachineIndex string, networkInterfaceName string,
|
||||
options *armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceOptions,
|
||||
) (armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceResponse, error) {
|
||||
return armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceResponse{
|
||||
InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceResult: armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceResult{
|
||||
Interface: a.getInterface,
|
||||
},
|
||||
}, a.getErr
|
||||
}
|
||||
|
||||
func (a *stubNetworkInterfacesAPI) Get(ctx context.Context, resourceGroupName string, networkInterfaceName string,
|
||||
options *armnetwork.InterfacesClientGetOptions,
|
||||
) (armnetwork.InterfacesClientGetResponse, error) {
|
||||
return armnetwork.InterfacesClientGetResponse{
|
||||
InterfacesClientGetResult: armnetwork.InterfacesClientGetResult{
|
||||
Interface: a.getInterface,
|
||||
},
|
||||
}, a.getErr
|
||||
}
|
||||
|
||||
type stubVirtualMachineScaleSetVMsClientListPager struct {
|
||||
pagesCounter int
|
||||
pages [][]*armcompute.VirtualMachineScaleSetVM
|
||||
}
|
||||
|
||||
func (p *stubVirtualMachineScaleSetVMsClientListPager) NextPage(ctx context.Context) bool {
|
||||
return p.pagesCounter < len(p.pages)
|
||||
}
|
||||
|
||||
func (p *stubVirtualMachineScaleSetVMsClientListPager) PageResponse() armcompute.VirtualMachineScaleSetVMsClientListResponse {
|
||||
if p.pagesCounter >= len(p.pages) {
|
||||
return armcompute.VirtualMachineScaleSetVMsClientListResponse{}
|
||||
}
|
||||
p.pagesCounter = p.pagesCounter + 1
|
||||
return armcompute.VirtualMachineScaleSetVMsClientListResponse{
|
||||
VirtualMachineScaleSetVMsClientListResult: armcompute.VirtualMachineScaleSetVMsClientListResult{
|
||||
VirtualMachineScaleSetVMListResult: armcompute.VirtualMachineScaleSetVMListResult{
|
||||
Value: p.pages[p.pagesCounter-1],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type stubVirtualMachineScaleSetVMsAPI struct {
|
||||
getVM armcompute.VirtualMachineScaleSetVM
|
||||
getErr error
|
||||
listPages [][]*armcompute.VirtualMachineScaleSetVM
|
||||
}
|
||||
|
||||
func (a *stubVirtualMachineScaleSetVMsAPI) Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string, options *armcompute.VirtualMachineScaleSetVMsClientGetOptions) (armcompute.VirtualMachineScaleSetVMsClientGetResponse, error) {
|
||||
return armcompute.VirtualMachineScaleSetVMsClientGetResponse{
|
||||
VirtualMachineScaleSetVMsClientGetResult: armcompute.VirtualMachineScaleSetVMsClientGetResult{
|
||||
VirtualMachineScaleSetVM: a.getVM,
|
||||
},
|
||||
}, a.getErr
|
||||
}
|
||||
|
||||
func (a *stubVirtualMachineScaleSetVMsAPI) List(resourceGroupName string, virtualMachineScaleSetName string, options *armcompute.VirtualMachineScaleSetVMsClientListOptions) virtualMachineScaleSetVMsClientListPager {
|
||||
return &stubVirtualMachineScaleSetVMsClientListPager{
|
||||
pages: a.listPages,
|
||||
}
|
||||
}
|
||||
|
||||
type stubVirtualMachineScaleSetsClientListPager struct {
|
||||
pagesCounter int
|
||||
pages [][]*armcompute.VirtualMachineScaleSet
|
||||
}
|
||||
|
||||
func (p *stubVirtualMachineScaleSetsClientListPager) NextPage(ctx context.Context) bool {
|
||||
return p.pagesCounter < len(p.pages)
|
||||
}
|
||||
|
||||
func (p *stubVirtualMachineScaleSetsClientListPager) PageResponse() armcompute.VirtualMachineScaleSetsClientListResponse {
|
||||
if p.pagesCounter >= len(p.pages) {
|
||||
return armcompute.VirtualMachineScaleSetsClientListResponse{}
|
||||
}
|
||||
p.pagesCounter = p.pagesCounter + 1
|
||||
return armcompute.VirtualMachineScaleSetsClientListResponse{
|
||||
VirtualMachineScaleSetsClientListResult: armcompute.VirtualMachineScaleSetsClientListResult{
|
||||
VirtualMachineScaleSetListResult: armcompute.VirtualMachineScaleSetListResult{
|
||||
Value: p.pages[p.pagesCounter-1],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type stubScaleSetsAPI struct {
|
||||
listPages [][]*armcompute.VirtualMachineScaleSet
|
||||
}
|
||||
|
||||
func (a *stubScaleSetsAPI) List(resourceGroupName string, options *armcompute.VirtualMachineScaleSetsClientListOptions) virtualMachineScaleSetsClientListPager {
|
||||
return &stubVirtualMachineScaleSetsClientListPager{
|
||||
pages: a.listPages,
|
||||
}
|
||||
}
|
||||
|
||||
type stubTagsAPI struct {
|
||||
createOrUpdateAtScopeErr error
|
||||
updateAtScopeErr error
|
||||
}
|
||||
|
||||
func (a *stubTagsAPI) CreateOrUpdateAtScope(ctx context.Context, scope string, parameters armresources.TagsResource, options *armresources.TagsClientCreateOrUpdateAtScopeOptions) (armresources.TagsClientCreateOrUpdateAtScopeResponse, error) {
|
||||
return armresources.TagsClientCreateOrUpdateAtScopeResponse{}, a.createOrUpdateAtScopeErr
|
||||
}
|
||||
|
||||
func (a *stubTagsAPI) UpdateAtScope(ctx context.Context, scope string, parameters armresources.TagsPatchResource, options *armresources.TagsClientUpdateAtScopeOptions) (armresources.TagsClientUpdateAtScopeResponse, error) {
|
||||
return armresources.TagsClientUpdateAtScopeResponse{}, a.updateAtScopeErr
|
||||
}
|
||||
|
||||
type stubSecurityGroupsClientListPager struct {
|
||||
pagesCounter int
|
||||
pages [][]*armnetwork.SecurityGroup
|
||||
}
|
||||
|
||||
func (p *stubSecurityGroupsClientListPager) NextPage(ctx context.Context) bool {
|
||||
return p.pagesCounter < len(p.pages)
|
||||
}
|
||||
|
||||
func (p *stubSecurityGroupsClientListPager) PageResponse() armnetwork.SecurityGroupsClientListResponse {
|
||||
if p.pagesCounter >= len(p.pages) {
|
||||
return armnetwork.SecurityGroupsClientListResponse{}
|
||||
}
|
||||
p.pagesCounter = p.pagesCounter + 1
|
||||
return armnetwork.SecurityGroupsClientListResponse{
|
||||
SecurityGroupsClientListResult: armnetwork.SecurityGroupsClientListResult{
|
||||
SecurityGroupListResult: armnetwork.SecurityGroupListResult{
|
||||
Value: p.pages[p.pagesCounter-1],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type stubSecurityGroupsAPI struct {
|
||||
listPages [][]*armnetwork.SecurityGroup
|
||||
}
|
||||
|
||||
func (a *stubSecurityGroupsAPI) List(resourceGroupName string, options *armnetwork.SecurityGroupsClientListOptions) securityGroupsClientListPager {
|
||||
return &stubSecurityGroupsClientListPager{
|
||||
pages: a.listPages,
|
||||
}
|
||||
}
|
||||
|
||||
type stubVirtualNetworksClientListPager struct {
|
||||
pagesCounter int
|
||||
pages [][]*armnetwork.VirtualNetwork
|
||||
}
|
||||
|
||||
func (p *stubVirtualNetworksClientListPager) NextPage(ctx context.Context) bool {
|
||||
return p.pagesCounter < len(p.pages)
|
||||
}
|
||||
|
||||
func (p *stubVirtualNetworksClientListPager) PageResponse() armnetwork.VirtualNetworksClientListResponse {
|
||||
if p.pagesCounter >= len(p.pages) {
|
||||
return armnetwork.VirtualNetworksClientListResponse{}
|
||||
}
|
||||
p.pagesCounter = p.pagesCounter + 1
|
||||
return armnetwork.VirtualNetworksClientListResponse{
|
||||
VirtualNetworksClientListResult: armnetwork.VirtualNetworksClientListResult{
|
||||
VirtualNetworkListResult: armnetwork.VirtualNetworkListResult{
|
||||
Value: p.pages[p.pagesCounter-1],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type stubVirtualNetworksAPI struct {
|
||||
listPages [][]*armnetwork.VirtualNetwork
|
||||
}
|
||||
|
||||
func (a *stubVirtualNetworksAPI) List(resourceGroupName string, options *armnetwork.VirtualNetworksClientListOptions) virtualNetworksClientListPager {
|
||||
return &stubVirtualNetworksClientListPager{
|
||||
pages: a.listPages,
|
||||
}
|
||||
}
|
||||
|
||||
type stubLoadBalancersClientListPager struct {
|
||||
pagesCounter int
|
||||
pages [][]*armnetwork.LoadBalancer
|
||||
}
|
||||
|
||||
func (p *stubLoadBalancersClientListPager) NextPage(ctx context.Context) bool {
|
||||
return p.pagesCounter < len(p.pages)
|
||||
}
|
||||
|
||||
func (p *stubLoadBalancersClientListPager) PageResponse() armnetwork.LoadBalancersClientListResponse {
|
||||
if p.pagesCounter >= len(p.pages) {
|
||||
return armnetwork.LoadBalancersClientListResponse{}
|
||||
}
|
||||
p.pagesCounter = p.pagesCounter + 1
|
||||
return armnetwork.LoadBalancersClientListResponse{
|
||||
LoadBalancersClientListResult: armnetwork.LoadBalancersClientListResult{
|
||||
LoadBalancerListResult: armnetwork.LoadBalancerListResult{
|
||||
Value: p.pages[p.pagesCounter-1],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type stubLoadBalancersAPI struct {
|
||||
listPages [][]*armnetwork.LoadBalancer
|
||||
}
|
||||
|
||||
func (a *stubLoadBalancersAPI) List(resourceGroupName string, options *armnetwork.LoadBalancersClientListOptions) loadBalancersClientListPager {
|
||||
return &stubLoadBalancersClientListPager{
|
||||
pages: a.listPages,
|
||||
}
|
||||
}
|
||||
|
||||
type stubPublicIPAddressesAPI struct {
|
||||
getResponse armnetwork.PublicIPAddressesClientGetResponse
|
||||
getVirtualMachineScaleSetPublicIPAddressResponse armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse
|
||||
getErr error
|
||||
}
|
||||
|
||||
func (a *stubPublicIPAddressesAPI) Get(ctx context.Context, resourceGroupName string, publicIPAddressName string,
|
||||
options *armnetwork.PublicIPAddressesClientGetOptions,
|
||||
) (armnetwork.PublicIPAddressesClientGetResponse, error) {
|
||||
return a.getResponse, a.getErr
|
||||
}
|
||||
|
||||
func (a *stubPublicIPAddressesAPI) GetVirtualMachineScaleSetPublicIPAddress(ctx context.Context, resourceGroupName string, virtualMachineScaleSetName string,
|
||||
virtualmachineIndex string, networkInterfaceName string, IPConfigurationName string, publicIPAddressName string,
|
||||
options *armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressOptions,
|
||||
) (armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse, error) {
|
||||
return a.getVirtualMachineScaleSetPublicIPAddressResponse, a.getErr
|
||||
}
|
||||
123
bootstrapper/cloudprovider/azure/autoscaler.go
Normal file
123
bootstrapper/cloudprovider/azure/autoscaler.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/azureshared"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Autoscaler holds the Azure cluster-autoscaler configuration.
|
||||
type Autoscaler struct{}
|
||||
|
||||
// Name returns the cloud-provider name as used by k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Name() string {
|
||||
return "azure"
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Secrets(providerID string, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
subscriptionID, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
creds, err := azureshared.ApplicationCredentialsFromURI(cloudServiceAccountURI)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
return resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cluster-autoscaler-azure",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ClientID": []byte(creds.ClientID),
|
||||
"ClientSecret": []byte(creds.ClientSecret),
|
||||
"ResourceGroup": []byte(resourceGroup),
|
||||
"SubscriptionID": []byte(subscriptionID),
|
||||
"TenantID": []byte(creds.TenantID),
|
||||
"VMType": []byte("vmss"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{}
|
||||
}
|
||||
|
||||
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{
|
||||
{
|
||||
Name: "ARM_SUBSCRIPTION_ID",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
SecretKeyRef: &k8s.SecretKeySelector{
|
||||
Key: "SubscriptionID",
|
||||
LocalObjectReference: k8s.LocalObjectReference{Name: "cluster-autoscaler-azure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ARM_RESOURCE_GROUP",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
SecretKeyRef: &k8s.SecretKeySelector{
|
||||
Key: "ResourceGroup",
|
||||
LocalObjectReference: k8s.LocalObjectReference{Name: "cluster-autoscaler-azure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ARM_TENANT_ID",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
SecretKeyRef: &k8s.SecretKeySelector{
|
||||
Key: "TenantID",
|
||||
LocalObjectReference: k8s.LocalObjectReference{Name: "cluster-autoscaler-azure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ARM_CLIENT_ID",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
SecretKeyRef: &k8s.SecretKeySelector{
|
||||
Key: "ClientID",
|
||||
LocalObjectReference: k8s.LocalObjectReference{Name: "cluster-autoscaler-azure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ARM_CLIENT_SECRET",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
SecretKeyRef: &k8s.SecretKeySelector{
|
||||
Key: "ClientSecret",
|
||||
LocalObjectReference: k8s.LocalObjectReference{Name: "cluster-autoscaler-azure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ARM_VM_TYPE",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
SecretKeyRef: &k8s.SecretKeySelector{
|
||||
Key: "VMType",
|
||||
LocalObjectReference: k8s.LocalObjectReference{Name: "cluster-autoscaler-azure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Supported is used to determine if we support autoscaling for the cloud provider.
|
||||
func (a *Autoscaler) Supported() bool {
|
||||
return true
|
||||
}
|
||||
81
bootstrapper/cloudprovider/azure/autoscaler_test.go
Normal file
81
bootstrapper/cloudprovider/azure/autoscaler_test.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAutoscalerSecrets(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
cloudServiceAccountURI string
|
||||
wantSecrets resources.Secrets
|
||||
wantErr bool
|
||||
}{
|
||||
"Secrets works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret",
|
||||
wantSecrets: resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cluster-autoscaler-azure",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ClientID": []byte("client-id"),
|
||||
"ClientSecret": []byte("client-secret"),
|
||||
"ResourceGroup": []byte("resource-group"),
|
||||
"SubscriptionID": []byte("subscription-id"),
|
||||
"TenantID": []byte("tenant-id"),
|
||||
"VMType": []byte("vmss"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid providerID fails": {
|
||||
providerID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid cloudServiceAccountURI fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
cloudServiceAccountURI: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
autoscaler := Autoscaler{}
|
||||
secrets, err := autoscaler.Secrets(tc.providerID, tc.cloudServiceAccountURI)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantSecrets, secrets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrivialAutoscalerFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
autoscaler := Autoscaler{}
|
||||
|
||||
assert.NotEmpty(autoscaler.Name())
|
||||
assert.Empty(autoscaler.Volumes())
|
||||
assert.Empty(autoscaler.VolumeMounts())
|
||||
assert.NotEmpty(autoscaler.Env())
|
||||
assert.True(autoscaler.Supported())
|
||||
}
|
||||
183
bootstrapper/cloudprovider/azure/ccm.go
Normal file
183
bootstrapper/cloudprovider/azure/ccm.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/azureshared"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type ccmMetadata interface {
|
||||
GetNetworkSecurityGroupName(ctx context.Context) (string, error)
|
||||
GetLoadBalancerName(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// CloudControllerManager holds the Azure cloud-controller-manager configuration.
|
||||
type CloudControllerManager struct {
|
||||
metadata ccmMetadata
|
||||
}
|
||||
|
||||
func NewCloudControllerManager(metadata ccmMetadata) *CloudControllerManager {
|
||||
return &CloudControllerManager{
|
||||
metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Image returns the container image used to provide cloud-controller-manager for the cloud-provider.
|
||||
func (c *CloudControllerManager) Image() string {
|
||||
return cloudprovider.CloudControllerManagerImageAzure
|
||||
}
|
||||
|
||||
// Path returns the path used by cloud-controller-manager executable within the container image.
|
||||
func (c *CloudControllerManager) Path() string {
|
||||
return "cloud-controller-manager"
|
||||
}
|
||||
|
||||
// Name returns the cloud-provider name as used by k8s cloud-controller-manager (k8s.gcr.io/cloud-controller-manager).
|
||||
func (c *CloudControllerManager) Name() string {
|
||||
return "azure"
|
||||
}
|
||||
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-controller-manager command.
|
||||
func (c *CloudControllerManager) ExtraArgs() []string {
|
||||
return []string{
|
||||
"--controllers=*,-cloud-node",
|
||||
"--cloud-config=/etc/azure/azure.json",
|
||||
"--allocate-node-cidrs=false",
|
||||
"--configure-cloud-routes=true",
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigMaps returns a list of ConfigMaps to deploy together with the k8s cloud-controller-manager
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/configmap/ .
|
||||
func (c *CloudControllerManager) ConfigMaps(instance metadata.InstanceMetadata) (resources.ConfigMaps, error) {
|
||||
return resources.ConfigMaps{}, nil
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/secret/ .
|
||||
func (c *CloudControllerManager) Secrets(ctx context.Context, providerID string, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
// Azure CCM expects cloud provider config to contain cluster configuration and service principal client secrets
|
||||
// reference: https://kubernetes-sigs.github.io/cloud-provider-azure/install/configs/
|
||||
|
||||
subscriptionID, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
creds, err := azureshared.ApplicationCredentialsFromURI(cloudServiceAccountURI)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
|
||||
vmType := "standard"
|
||||
if _, _, _, _, err := azureshared.ScaleSetInformationFromProviderID(providerID); err == nil {
|
||||
vmType = "vmss"
|
||||
}
|
||||
|
||||
securityGroupName, err := c.metadata.GetNetworkSecurityGroupName(ctx)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
|
||||
loadBalancerName, err := c.metadata.GetLoadBalancerName(ctx)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
|
||||
config := cloudConfig{
|
||||
Cloud: "AzurePublicCloud",
|
||||
TenantID: creds.TenantID,
|
||||
SubscriptionID: subscriptionID,
|
||||
ResourceGroup: resourceGroup,
|
||||
LoadBalancerSku: "standard",
|
||||
SecurityGroupName: securityGroupName,
|
||||
LoadBalancerName: loadBalancerName,
|
||||
UseInstanceMetadata: true,
|
||||
VmType: vmType,
|
||||
Location: creds.Location,
|
||||
AADClientID: creds.ClientID,
|
||||
AADClientSecret: creds.ClientSecret,
|
||||
}
|
||||
|
||||
rawConfig, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
|
||||
return resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "azureconfig",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"azure.json": rawConfig,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/storage/volumes/ .
|
||||
func (c *CloudControllerManager) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{
|
||||
{
|
||||
Name: "azureconfig",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
Secret: &k8s.SecretVolumeSource{
|
||||
SecretName: "azureconfig",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VolumeMounts a list of of volume mounts to deploy together with the k8s cloud-controller-manager.
|
||||
func (c *CloudControllerManager) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{
|
||||
{
|
||||
Name: "azureconfig",
|
||||
ReadOnly: true,
|
||||
MountPath: "/etc/azure",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cloud-controller-manager.
|
||||
func (c *CloudControllerManager) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{}
|
||||
}
|
||||
|
||||
// Supported is used to determine if cloud controller manager is implemented for this cloud provider.
|
||||
func (c *CloudControllerManager) Supported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type cloudConfig struct {
|
||||
Cloud string `json:"cloud,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||
ResourceGroup string `json:"resourceGroup,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
SubnetName string `json:"subnetName,omitempty"`
|
||||
SecurityGroupName string `json:"securityGroupName,omitempty"`
|
||||
SecurityGroupResourceGroup string `json:"securityGroupResourceGroup,omitempty"`
|
||||
LoadBalancerName string `json:"loadBalancerName,omitempty"`
|
||||
LoadBalancerSku string `json:"loadBalancerSku,omitempty"`
|
||||
VNetName string `json:"vnetName,omitempty"`
|
||||
VNetResourceGroup string `json:"vnetResourceGroup,omitempty"`
|
||||
CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty"`
|
||||
UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty"`
|
||||
VmType string `json:"vmType,omitempty"`
|
||||
AADClientID string `json:"aadClientId,omitempty"`
|
||||
AADClientSecret string `json:"aadClientSecret,omitempty"`
|
||||
}
|
||||
136
bootstrapper/cloudprovider/azure/ccm_test.go
Normal file
136
bootstrapper/cloudprovider/azure/ccm_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
someErr := errors.New("some error")
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
metadata ccmMetadata
|
||||
cloudServiceAccountURI string
|
||||
wantSecrets resources.Secrets
|
||||
wantErr bool
|
||||
}{
|
||||
"Secrets works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location",
|
||||
metadata: &ccmMetadataStub{loadBalancerName: "load-balancer-name", networkSecurityGroupName: "network-security-group-name"},
|
||||
wantSecrets: resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "azureconfig",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"azure.json": []byte(`{"cloud":"AzurePublicCloud","tenantId":"tenant-id","subscriptionId":"subscription-id","resourceGroup":"resource-group","location":"location","securityGroupName":"network-security-group-name","loadBalancerName":"load-balancer-name","loadBalancerSku":"standard","useInstanceMetadata":true,"vmType":"standard","aadClientId":"client-id","aadClientSecret":"client-secret"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Secrets works for scale sets": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location",
|
||||
metadata: &ccmMetadataStub{loadBalancerName: "load-balancer-name", networkSecurityGroupName: "network-security-group-name"},
|
||||
wantSecrets: resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "azureconfig",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"azure.json": []byte(`{"cloud":"AzurePublicCloud","tenantId":"tenant-id","subscriptionId":"subscription-id","resourceGroup":"resource-group","location":"location","securityGroupName":"network-security-group-name","loadBalancerName":"load-balancer-name","loadBalancerSku":"standard","useInstanceMetadata":true,"vmType":"vmss","aadClientId":"client-id","aadClientSecret":"client-secret"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"cannot get load balancer Name": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location",
|
||||
metadata: &ccmMetadataStub{getLoadBalancerNameErr: someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"cannot get network security group name": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret&location=location",
|
||||
metadata: &ccmMetadataStub{getNetworkSecurityGroupNameErr: someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid providerID fails": {
|
||||
providerID: "invalid",
|
||||
metadata: &ccmMetadataStub{},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid cloudServiceAccountURI fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
metadata: &ccmMetadataStub{},
|
||||
cloudServiceAccountURI: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
cloud := NewCloudControllerManager(tc.metadata)
|
||||
secrets, err := cloud.Secrets(context.Background(), tc.providerID, tc.cloudServiceAccountURI)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantSecrets, secrets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrivialCCMFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cloud := CloudControllerManager{}
|
||||
|
||||
assert.NotEmpty(cloud.Image())
|
||||
assert.NotEmpty(cloud.Path())
|
||||
assert.NotEmpty(cloud.Name())
|
||||
assert.NotEmpty(cloud.ExtraArgs())
|
||||
assert.Empty(cloud.ConfigMaps(metadata.InstanceMetadata{}))
|
||||
assert.NotEmpty(cloud.Volumes())
|
||||
assert.NotEmpty(cloud.VolumeMounts())
|
||||
assert.Empty(cloud.Env())
|
||||
assert.True(cloud.Supported())
|
||||
}
|
||||
|
||||
type ccmMetadataStub struct {
|
||||
networkSecurityGroupName string
|
||||
loadBalancerName string
|
||||
|
||||
getNetworkSecurityGroupNameErr error
|
||||
getLoadBalancerNameErr error
|
||||
}
|
||||
|
||||
func (c *ccmMetadataStub) GetNetworkSecurityGroupName(ctx context.Context) (string, error) {
|
||||
return c.networkSecurityGroupName, c.getNetworkSecurityGroupNameErr
|
||||
}
|
||||
|
||||
func (c *ccmMetadataStub) GetLoadBalancerName(ctx context.Context) (string, error) {
|
||||
return c.loadBalancerName, c.getLoadBalancerNameErr
|
||||
}
|
||||
29
bootstrapper/cloudprovider/azure/cloudnodemanager.go
Normal file
29
bootstrapper/cloudprovider/azure/cloudnodemanager.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package azure
|
||||
|
||||
import "github.com/edgelesssys/constellation/bootstrapper/cloudprovider"
|
||||
|
||||
// CloudNodeManager holds the Azure cloud-node-manager configuration.
|
||||
// reference: https://raw.githubusercontent.com/kubernetes-sigs/cloud-provider-azure/master/examples/out-of-tree/cloud-node-manager.yaml .
|
||||
type CloudNodeManager struct{}
|
||||
|
||||
// Image returns the container image used to provide cloud-node-manager for the cloud-provider.
|
||||
func (c *CloudNodeManager) Image() string {
|
||||
return cloudprovider.CloudNodeManagerImageAzure
|
||||
}
|
||||
|
||||
// Path returns the path used by cloud-node-manager executable within the container image.
|
||||
func (c *CloudNodeManager) Path() string {
|
||||
return "cloud-node-manager"
|
||||
}
|
||||
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-node-manager command.
|
||||
func (c *CloudNodeManager) ExtraArgs() []string {
|
||||
return []string{
|
||||
"--wait-routes=true",
|
||||
}
|
||||
}
|
||||
|
||||
// Supported is used to determine if cloud node manager is implemented for this cloud provider.
|
||||
func (c *CloudNodeManager) Supported() bool {
|
||||
return true
|
||||
}
|
||||
17
bootstrapper/cloudprovider/azure/cloudnodemanager_test.go
Normal file
17
bootstrapper/cloudprovider/azure/cloudnodemanager_test.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrivialCNMFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cloud := CloudNodeManager{}
|
||||
|
||||
assert.NotEmpty(cloud.Image())
|
||||
assert.NotEmpty(cloud.Path())
|
||||
assert.NotEmpty(cloud.ExtraArgs())
|
||||
assert.True(cloud.Supported())
|
||||
}
|
||||
54
bootstrapper/cloudprovider/azure/imds.go
Normal file
54
bootstrapper/cloudprovider/azure/imds.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// subset of azure imds API: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux
|
||||
// this is not yet available through the azure sdk (see https://github.com/Azure/azure-rest-api-specs/issues/4408)
|
||||
|
||||
const (
|
||||
imdsURL = "http://169.254.169.254/metadata/instance"
|
||||
imdsAPIVersion = "2021-02-01"
|
||||
)
|
||||
|
||||
type imdsClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Retrieve retrieves instance metadata from the azure imds API.
|
||||
func (c *imdsClient) Retrieve(ctx context.Context) (metadataResponse, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", imdsURL, http.NoBody)
|
||||
if err != nil {
|
||||
return metadataResponse{}, err
|
||||
}
|
||||
req.Header.Add("Metadata", "True")
|
||||
query := req.URL.Query()
|
||||
query.Add("format", "json")
|
||||
query.Add("api-version", imdsAPIVersion)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return metadataResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return metadataResponse{}, err
|
||||
}
|
||||
var res metadataResponse
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return metadataResponse{}, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// metadataResponse contains metadataResponse with only the required values.
|
||||
type metadataResponse struct {
|
||||
Compute struct {
|
||||
ResourceID string `json:"resourceId,omitempty"`
|
||||
} `json:"compute,omitempty"`
|
||||
}
|
||||
120
bootstrapper/cloudprovider/azure/imds_test.go
Normal file
120
bootstrapper/cloudprovider/azure/imds_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
response := metadataResponse{
|
||||
Compute: struct {
|
||||
ResourceID string `json:"resourceId,omitempty"`
|
||||
}{
|
||||
ResourceID: "resource-id",
|
||||
},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
server httpBufconnServer
|
||||
wantErr bool
|
||||
wantResponse metadataResponse
|
||||
}{
|
||||
"metadata response parsed": {
|
||||
server: newHTTPBufconnServerWithMetadataResponse(response),
|
||||
wantResponse: response,
|
||||
},
|
||||
"invalid imds response detected": {
|
||||
server: newHTTPBufconnServer(func(writer http.ResponseWriter, request *http.Request) {
|
||||
fmt.Fprintln(writer, "invalid-result")
|
||||
}),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
defer tc.server.Close()
|
||||
|
||||
hClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: tc.server.DialContext,
|
||||
Dial: tc.server.Dial,
|
||||
DialTLSContext: tc.server.DialContext,
|
||||
DialTLS: tc.server.Dial,
|
||||
},
|
||||
}
|
||||
iClient := imdsClient{
|
||||
client: &hClient,
|
||||
}
|
||||
resp, err := iClient.Retrieve(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantResponse, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type httpBufconnServer struct {
|
||||
*httptest.Server
|
||||
*bufconn.Listener
|
||||
}
|
||||
|
||||
func (s *httpBufconnServer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return s.Listener.DialContext(ctx)
|
||||
}
|
||||
|
||||
func (s *httpBufconnServer) Dial(network, addr string) (net.Conn, error) {
|
||||
return s.Listener.Dial()
|
||||
}
|
||||
|
||||
func (s *httpBufconnServer) Close() {
|
||||
s.Server.Close()
|
||||
s.Listener.Close()
|
||||
}
|
||||
|
||||
func newHTTPBufconnServer(handlerFunc http.HandlerFunc) httpBufconnServer {
|
||||
server := httptest.NewUnstartedServer(handlerFunc)
|
||||
listener := bufconn.Listen(1024)
|
||||
server.Listener = listener
|
||||
server.Start()
|
||||
return httpBufconnServer{
|
||||
Server: server,
|
||||
Listener: listener,
|
||||
}
|
||||
}
|
||||
|
||||
func newHTTPBufconnServerWithMetadataResponse(res metadataResponse) httpBufconnServer {
|
||||
return newHTTPBufconnServer(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Host != "169.254.169.254" || request.Header.Get("Metadata") != "True" || request.URL.Query().Get("format") != "json" || request.URL.Query().Get("api-version") != imdsAPIVersion {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := writer.Write([]byte("error"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
rawResp, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = writer.Write(rawResp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
58
bootstrapper/cloudprovider/azure/logger.go
Normal file
58
bootstrapper/cloudprovider/azure/logger.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights"
|
||||
"github.com/edgelesssys/constellation/internal/azureshared"
|
||||
"github.com/microsoft/ApplicationInsights-Go/appinsights"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
client appinsights.TelemetryClient
|
||||
}
|
||||
|
||||
// NewLogger creates a new client to store information in Azure Application Insights
|
||||
// https://github.com/Microsoft/ApplicationInsights-go
|
||||
func NewLogger(ctx context.Context, metadata *Metadata) (*Logger, error) {
|
||||
providerID, err := metadata.providerID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uid, err := azureshared.UIDFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceName := "constellation-insights-" + uid
|
||||
resp, err := metadata.applicationInsightsAPI.Get(ctx, resourceGroup, resourceName, &armapplicationinsights.ComponentsClientGetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Properties == nil || resp.Properties.InstrumentationKey == nil {
|
||||
return nil, errors.New("unable to get instrumentation key")
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
client: appinsights.NewTelemetryClient(*resp.Properties.InstrumentationKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Disclose stores log information in Azure Application Insights!
|
||||
// Do **NOT** log sensitive information!
|
||||
func (l *Logger) Disclose(msg string) {
|
||||
l.client.Track(appinsights.NewTraceTelemetry(msg, appinsights.Information))
|
||||
}
|
||||
|
||||
// Close blocks until all information are written to cloud API.
|
||||
func (l *Logger) Close() error {
|
||||
<-l.client.Channel().Close()
|
||||
return nil
|
||||
}
|
||||
285
bootstrapper/cloudprovider/azure/metadata.go
Normal file
285
bootstrapper/cloudprovider/azure/metadata.go
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
"github.com/edgelesssys/constellation/internal/azureshared"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
publicIPAddressRegexp = regexp.MustCompile(`/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft.Network/publicIPAddresses/(?P<IPname>[^/]+)`)
|
||||
keyPathRegexp = regexp.MustCompile(`^\/home\/([^\/]+)\/\.ssh\/authorized_keys$`)
|
||||
)
|
||||
|
||||
// Metadata implements azure metadata APIs.
|
||||
type Metadata struct {
|
||||
imdsAPI
|
||||
virtualNetworksAPI
|
||||
securityGroupsAPI
|
||||
networkInterfacesAPI
|
||||
publicIPAddressesAPI
|
||||
scaleSetsAPI
|
||||
loadBalancerAPI
|
||||
virtualMachineScaleSetVMsAPI
|
||||
tagsAPI
|
||||
applicationInsightsAPI
|
||||
}
|
||||
|
||||
// NewMetadata creates a new Metadata.
|
||||
func NewMetadata(ctx context.Context) (*Metadata, error) {
|
||||
cred, err := azidentity.NewDefaultAzureCredential(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The default http client may use a system-wide proxy and it is recommended to disable the proxy explicitly:
|
||||
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#proxies
|
||||
// See also: https://github.com/microsoft/azureimds/blob/master/imdssample.go#L10
|
||||
imdsAPI := imdsClient{
|
||||
client: &http.Client{Transport: &http.Transport{Proxy: nil}},
|
||||
}
|
||||
instanceMetadata, err := imdsAPI.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subscriptionID, _, err := azureshared.BasicsFromProviderID("azure://" + instanceMetadata.Compute.ResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
virtualNetworksAPI := armnetwork.NewVirtualNetworksClient(subscriptionID, cred, nil)
|
||||
networkInterfacesAPI := armnetwork.NewInterfacesClient(subscriptionID, cred, nil)
|
||||
publicIPAddressesAPI := armnetwork.NewPublicIPAddressesClient(subscriptionID, cred, nil)
|
||||
securityGroupsAPI := armnetwork.NewSecurityGroupsClient(subscriptionID, cred, nil)
|
||||
scaleSetsAPI := armcompute.NewVirtualMachineScaleSetsClient(subscriptionID, cred, nil)
|
||||
loadBalancerAPI := armnetwork.NewLoadBalancersClient(subscriptionID, cred, nil)
|
||||
virtualMachineScaleSetVMsAPI := armcompute.NewVirtualMachineScaleSetVMsClient(subscriptionID, cred, nil)
|
||||
tagsAPI := armresources.NewTagsClient(subscriptionID, cred, nil)
|
||||
applicationInsightsAPI := armapplicationinsights.NewComponentsClient(subscriptionID, cred, nil)
|
||||
|
||||
return &Metadata{
|
||||
imdsAPI: &imdsAPI,
|
||||
virtualNetworksAPI: &virtualNetworksClient{virtualNetworksAPI},
|
||||
networkInterfacesAPI: &networkInterfacesClient{networkInterfacesAPI},
|
||||
securityGroupsAPI: &securityGroupsClient{securityGroupsAPI},
|
||||
publicIPAddressesAPI: &publicIPAddressesClient{publicIPAddressesAPI},
|
||||
loadBalancerAPI: &loadBalancersClient{loadBalancerAPI},
|
||||
scaleSetsAPI: &scaleSetsClient{scaleSetsAPI},
|
||||
virtualMachineScaleSetVMsAPI: &virtualMachineScaleSetVMsClient{virtualMachineScaleSetVMsAPI},
|
||||
tagsAPI: &tagsClient{tagsAPI},
|
||||
applicationInsightsAPI: &applicationInsightsClient{applicationInsightsAPI},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List retrieves all instances belonging to the current constellation.
|
||||
func (m *Metadata) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
providerID, err := m.providerID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scaleSetInstances, err := m.listScaleSetVMs(ctx, resourceGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scaleSetInstances, nil
|
||||
}
|
||||
|
||||
// Self retrieves the current instance.
|
||||
func (m *Metadata) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||
providerID, err := m.providerID(ctx)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
return m.GetInstance(ctx, providerID)
|
||||
}
|
||||
|
||||
// GetInstance retrieves an instance using its providerID.
|
||||
func (m *Metadata) GetInstance(ctx context.Context, providerID string) (metadata.InstanceMetadata, error) {
|
||||
instance, scaleSetErr := m.getScaleSetVM(ctx, providerID)
|
||||
if scaleSetErr == nil {
|
||||
return instance, nil
|
||||
}
|
||||
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving instance given providerID %v: %w", providerID, scaleSetErr)
|
||||
}
|
||||
|
||||
// GetNetworkSecurityGroupName returns the security group name of the resource group.
|
||||
func (m *Metadata) GetNetworkSecurityGroupName(ctx context.Context) (string, error) {
|
||||
providerID, err := m.providerID(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nsg, err := m.getNetworkSecurityGroup(ctx, resourceGroup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if nsg == nil || nsg.Name == nil {
|
||||
return "", fmt.Errorf("could not dereference network security group name")
|
||||
}
|
||||
return *nsg.Name, nil
|
||||
}
|
||||
|
||||
// GetSubnetworkCIDR retrieves the subnetwork CIDR from cloud provider metadata.
|
||||
func (m *Metadata) GetSubnetworkCIDR(ctx context.Context) (string, error) {
|
||||
providerID, err := m.providerID(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
virtualNetwork, err := m.getVirtualNetwork(ctx, resourceGroup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if virtualNetwork == nil || virtualNetwork.Properties == nil || len(virtualNetwork.Properties.Subnets) == 0 ||
|
||||
virtualNetwork.Properties.Subnets[0].Properties == nil || virtualNetwork.Properties.Subnets[0].Properties.AddressPrefix == nil {
|
||||
return "", fmt.Errorf("could not retrieve subnetwork CIDR from virtual network %v", virtualNetwork)
|
||||
}
|
||||
|
||||
return *virtualNetwork.Properties.Subnets[0].Properties.AddressPrefix, nil
|
||||
}
|
||||
|
||||
// getLoadBalancer retrieves the load balancer from cloud provider metadata.
|
||||
func (m *Metadata) getLoadBalancer(ctx context.Context) (*armnetwork.LoadBalancer, error) {
|
||||
providerID, err := m.providerID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pager := m.loadBalancerAPI.List(resourceGroup, nil)
|
||||
|
||||
for pager.NextPage(ctx) {
|
||||
for _, lb := range pager.PageResponse().Value {
|
||||
if lb != nil && lb.Properties != nil {
|
||||
return lb, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not get any load balancer")
|
||||
}
|
||||
|
||||
// SupportsLoadBalancer returns true if the cloud provider supports load balancers.
|
||||
func (m *Metadata) SupportsLoadBalancer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetLoadBalancerName returns the load balancer name of the resource group.
|
||||
func (m *Metadata) GetLoadBalancerName(ctx context.Context) (string, error) {
|
||||
lb, err := m.getLoadBalancer(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lb == nil || lb.Name == nil {
|
||||
return "", fmt.Errorf("could not dereference load balancer name")
|
||||
}
|
||||
return *lb.Name, nil
|
||||
}
|
||||
|
||||
// GetLoadBalancerIP retrieves the first load balancer IP from cloud provider metadata.
|
||||
func (m *Metadata) GetLoadBalancerIP(ctx context.Context) (string, error) {
|
||||
lb, err := m.getLoadBalancer(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lb == nil || lb.Properties == nil {
|
||||
return "", fmt.Errorf("could not dereference load balancer IP configuration")
|
||||
}
|
||||
|
||||
var pubIPID string
|
||||
for _, fipConf := range lb.Properties.FrontendIPConfigurations {
|
||||
if fipConf == nil || fipConf.Properties == nil || fipConf.Properties.PublicIPAddress == nil || fipConf.Properties.PublicIPAddress.ID == nil {
|
||||
continue
|
||||
}
|
||||
pubIPID = *fipConf.Properties.PublicIPAddress.ID
|
||||
break
|
||||
}
|
||||
|
||||
if pubIPID == "" {
|
||||
return "", fmt.Errorf("could not find public IP address reference in load balancer")
|
||||
}
|
||||
|
||||
matches := publicIPAddressRegexp.FindStringSubmatch(pubIPID)
|
||||
if len(matches) != 2 {
|
||||
return "", fmt.Errorf("could not find public IP address name in load balancer: %v", pubIPID)
|
||||
}
|
||||
pubIPName := matches[1]
|
||||
|
||||
providerID, err := m.providerID(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := m.publicIPAddressesAPI.Get(ctx, resourceGroup, pubIPName, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not retrieve public IP address: %w", err)
|
||||
}
|
||||
if resp.Properties == nil || resp.Properties.IPAddress == nil {
|
||||
return "", fmt.Errorf("could not resolve public IP address reference for load balancer")
|
||||
}
|
||||
return *resp.Properties.IPAddress, nil
|
||||
}
|
||||
|
||||
// Supported is used to determine if metadata API is implemented for this cloud provider.
|
||||
func (m *Metadata) Supported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// providerID retrieves the current instances providerID.
|
||||
func (m *Metadata) providerID(ctx context.Context) (string, error) {
|
||||
instanceMetadata, err := m.imdsAPI.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "azure://" + instanceMetadata.Compute.ResourceID, nil
|
||||
}
|
||||
|
||||
// extractInstanceTags converts azure tags into metadata key-value pairs.
|
||||
func extractInstanceTags(tags map[string]*string) map[string]string {
|
||||
metadataMap := map[string]string{}
|
||||
for key, value := range tags {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
metadataMap[key] = *value
|
||||
}
|
||||
return metadataMap
|
||||
}
|
||||
|
||||
// extractSSHKeys extracts SSH public keys from azure instance OS Profile.
|
||||
func extractSSHKeys(sshConfig armcompute.SSHConfiguration) map[string][]string {
|
||||
sshKeys := map[string][]string{}
|
||||
for _, key := range sshConfig.PublicKeys {
|
||||
if key == nil || key.Path == nil || key.KeyData == nil {
|
||||
continue
|
||||
}
|
||||
matches := keyPathRegexp.FindStringSubmatch(*key.Path)
|
||||
if len(matches) != 2 {
|
||||
continue
|
||||
}
|
||||
sshKeys[matches[1]] = append(sshKeys[matches[1]], *key.KeyData)
|
||||
}
|
||||
return sshKeys
|
||||
}
|
||||
757
bootstrapper/cloudprovider/azure/metadata_test.go
Normal file
757
bootstrapper/cloudprovider/azure/metadata_test.go
Normal file
|
|
@ -0,0 +1,757 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
wantInstances := []metadata.InstanceMetadata{
|
||||
{
|
||||
Name: "scale-set-name-instance-id",
|
||||
ProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
SSHKeys: map[string][]string{"user": {"key-data"}},
|
||||
},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
imdsAPI imdsAPI
|
||||
networkInterfacesAPI networkInterfacesAPI
|
||||
scaleSetsAPI scaleSetsAPI
|
||||
virtualMachineScaleSetVMsAPI virtualMachineScaleSetVMsAPI
|
||||
tagsAPI tagsAPI
|
||||
wantErr bool
|
||||
wantInstances []metadata.InstanceMetadata
|
||||
}{
|
||||
"List works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
scaleSetsAPI: newScaleSetsStub(),
|
||||
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
|
||||
tagsAPI: newTagsStub(),
|
||||
wantInstances: wantInstances,
|
||||
},
|
||||
"providerID cannot be retrieved": {
|
||||
imdsAPI: &stubIMDSAPI{retrieveErr: errors.New("imds err")},
|
||||
wantErr: true,
|
||||
},
|
||||
"providerID cannot be parsed": {
|
||||
imdsAPI: newInvalidIMDSStub(),
|
||||
wantErr: true,
|
||||
},
|
||||
"listScaleSetVMs fails": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
scaleSetsAPI: newScaleSetsStub(),
|
||||
virtualMachineScaleSetVMsAPI: newFailingListsVirtualMachineScaleSetsVMsStub(),
|
||||
tagsAPI: newTagsStub(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
azureMetadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
networkInterfacesAPI: tc.networkInterfacesAPI,
|
||||
scaleSetsAPI: tc.scaleSetsAPI,
|
||||
virtualMachineScaleSetVMsAPI: tc.virtualMachineScaleSetVMsAPI,
|
||||
tagsAPI: tc.tagsAPI,
|
||||
}
|
||||
instances, err := azureMetadata.List(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.ElementsMatch(tc.wantInstances, instances)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelf(t *testing.T) {
|
||||
wantVMInstance := metadata.InstanceMetadata{
|
||||
Name: "instance-name",
|
||||
ProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
SSHKeys: map[string][]string{"user": {"key-data"}},
|
||||
}
|
||||
wantScaleSetInstance := metadata.InstanceMetadata{
|
||||
Name: "scale-set-name-instance-id",
|
||||
ProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
SSHKeys: map[string][]string{"user": {"key-data"}},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
imdsAPI imdsAPI
|
||||
networkInterfacesAPI networkInterfacesAPI
|
||||
virtualMachineScaleSetVMsAPI virtualMachineScaleSetVMsAPI
|
||||
wantErr bool
|
||||
wantInstance metadata.InstanceMetadata
|
||||
}{
|
||||
"self for individual instance works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
|
||||
wantInstance: wantVMInstance,
|
||||
},
|
||||
"self for scale set instance works": {
|
||||
imdsAPI: newScaleSetIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
|
||||
wantInstance: wantScaleSetInstance,
|
||||
},
|
||||
"providerID cannot be retrieved": {
|
||||
imdsAPI: &stubIMDSAPI{retrieveErr: errors.New("imds err")},
|
||||
wantErr: true,
|
||||
},
|
||||
"GetInstance fails": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
networkInterfacesAPI: tc.networkInterfacesAPI,
|
||||
virtualMachineScaleSetVMsAPI: tc.virtualMachineScaleSetVMsAPI,
|
||||
}
|
||||
instance, err := metadata.Self(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantInstance, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworkSecurityGroupName(t *testing.T) {
|
||||
name := "network-security-group-name"
|
||||
testCases := map[string]struct {
|
||||
securityGroupsAPI securityGroupsAPI
|
||||
imdsAPI imdsAPI
|
||||
wantName string
|
||||
wantErr bool
|
||||
}{
|
||||
"GetNetworkSecurityGroupName works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
securityGroupsAPI: &stubSecurityGroupsAPI{
|
||||
listPages: [][]*armnetwork.SecurityGroup{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(name),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantName: name,
|
||||
},
|
||||
"no security group": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
securityGroupsAPI: &stubSecurityGroupsAPI{},
|
||||
wantErr: true,
|
||||
},
|
||||
"missing name in security group struct": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
securityGroupsAPI: &stubSecurityGroupsAPI{listPages: [][]*armnetwork.SecurityGroup{{{}}}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
securityGroupsAPI: tc.securityGroupsAPI,
|
||||
}
|
||||
name, err := metadata.GetNetworkSecurityGroupName(context.Background())
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantName, name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSubnetworkCIDR(t *testing.T) {
|
||||
subnetworkCIDR := "192.0.2.0/24"
|
||||
name := "name"
|
||||
testCases := map[string]struct {
|
||||
virtualNetworksAPI virtualNetworksAPI
|
||||
imdsAPI imdsAPI
|
||||
wantNetworkCIDR string
|
||||
wantErr bool
|
||||
}{
|
||||
"GetSubnetworkCIDR works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
virtualNetworksAPI: &stubVirtualNetworksAPI{listPages: [][]*armnetwork.VirtualNetwork{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(name),
|
||||
Properties: &armnetwork.VirtualNetworkPropertiesFormat{
|
||||
Subnets: []*armnetwork.Subnet{
|
||||
{Properties: &armnetwork.SubnetPropertiesFormat{AddressPrefix: to.StringPtr(subnetworkCIDR)}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
wantNetworkCIDR: subnetworkCIDR,
|
||||
},
|
||||
"no virtual networks found": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
virtualNetworksAPI: &stubVirtualNetworksAPI{listPages: [][]*armnetwork.VirtualNetwork{
|
||||
{},
|
||||
}},
|
||||
wantErr: true,
|
||||
wantNetworkCIDR: subnetworkCIDR,
|
||||
},
|
||||
"malformed network struct": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
virtualNetworksAPI: &stubVirtualNetworksAPI{listPages: [][]*armnetwork.VirtualNetwork{
|
||||
{
|
||||
{},
|
||||
},
|
||||
}},
|
||||
wantErr: true,
|
||||
wantNetworkCIDR: subnetworkCIDR,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
virtualNetworksAPI: tc.virtualNetworksAPI,
|
||||
}
|
||||
subnetworkCIDR, err := metadata.GetSubnetworkCIDR(context.Background())
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantNetworkCIDR, subnetworkCIDR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLoadBalancerName(t *testing.T) {
|
||||
loadBalancerName := "load-balancer-name"
|
||||
testCases := map[string]struct {
|
||||
loadBalancerAPI loadBalancerAPI
|
||||
imdsAPI imdsAPI
|
||||
wantName string
|
||||
wantErr bool
|
||||
}{
|
||||
"GetLoadBalancerName works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(loadBalancerName),
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantName: loadBalancerName,
|
||||
},
|
||||
"invalid load balancer struct": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{listPages: [][]*armnetwork.LoadBalancer{{{}}}},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid missing name": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{listPages: [][]*armnetwork.LoadBalancer{{{
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{},
|
||||
}}}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
loadBalancerAPI: tc.loadBalancerAPI,
|
||||
}
|
||||
loadbalancerName, err := metadata.GetLoadBalancerName(context.Background())
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantName, loadbalancerName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLoadBalancerIP(t *testing.T) {
|
||||
loadBalancerName := "load-balancer-name"
|
||||
publicIP := "192.0.2.1"
|
||||
correctPublicIPID := "/subscriptions/subscription/resourceGroups/resourceGroup/providers/Microsoft.Network/publicIPAddresses/pubIPName"
|
||||
someErr := errors.New("some error")
|
||||
testCases := map[string]struct {
|
||||
loadBalancerAPI loadBalancerAPI
|
||||
publicIPAddressesAPI publicIPAddressesAPI
|
||||
imdsAPI imdsAPI
|
||||
wantIP string
|
||||
wantErr bool
|
||||
}{
|
||||
"GetLoadBalancerIP works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(loadBalancerName),
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{
|
||||
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
|
||||
PublicIPAddress: &armnetwork.PublicIPAddress{
|
||||
ID: &correctPublicIPID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getResponse: armnetwork.PublicIPAddressesClientGetResponse{
|
||||
PublicIPAddressesClientGetResult: armnetwork.PublicIPAddressesClientGetResult{
|
||||
PublicIPAddress: armnetwork.PublicIPAddress{
|
||||
Properties: &armnetwork.PublicIPAddressPropertiesFormat{
|
||||
IPAddress: &publicIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
wantIP: publicIP,
|
||||
},
|
||||
"no load balancer": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"load balancer missing public IP reference": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(loadBalancerName),
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{
|
||||
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"public IP reference has wrong format": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(loadBalancerName),
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{
|
||||
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
|
||||
PublicIPAddress: &armnetwork.PublicIPAddress{
|
||||
ID: to.StringPtr("wrong-format"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"no public IP address found": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(loadBalancerName),
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{
|
||||
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
|
||||
PublicIPAddress: &armnetwork.PublicIPAddress{
|
||||
ID: &correctPublicIPID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getErr: someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"found public IP has no address field": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
loadBalancerAPI: &stubLoadBalancersAPI{
|
||||
listPages: [][]*armnetwork.LoadBalancer{
|
||||
{
|
||||
{
|
||||
Name: to.StringPtr(loadBalancerName),
|
||||
Properties: &armnetwork.LoadBalancerPropertiesFormat{
|
||||
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
|
||||
PublicIPAddress: &armnetwork.PublicIPAddress{
|
||||
ID: &correctPublicIPID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getResponse: armnetwork.PublicIPAddressesClientGetResponse{
|
||||
PublicIPAddressesClientGetResult: armnetwork.PublicIPAddressesClientGetResult{
|
||||
PublicIPAddress: armnetwork.PublicIPAddress{
|
||||
Properties: &armnetwork.PublicIPAddressPropertiesFormat{},
|
||||
},
|
||||
},
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
loadBalancerAPI: tc.loadBalancerAPI,
|
||||
publicIPAddressesAPI: tc.publicIPAddressesAPI,
|
||||
}
|
||||
loadbalancerName, err := metadata.GetLoadBalancerIP(context.Background())
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantIP, loadbalancerName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataSupported(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
metadata := Metadata{}
|
||||
assert.True(metadata.Supported())
|
||||
}
|
||||
|
||||
func TestProviderID(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
imdsAPI imdsAPI
|
||||
wantErr bool
|
||||
wantProviderID string
|
||||
}{
|
||||
"providerID for individual instance works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
wantProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
},
|
||||
"providerID for scale set instance works": {
|
||||
imdsAPI: newScaleSetIMDSStub(),
|
||||
wantProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
},
|
||||
"imds retrieval fails": {
|
||||
imdsAPI: &stubIMDSAPI{retrieveErr: errors.New("imds err")},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
}
|
||||
providerID, err := metadata.providerID(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantProviderID, providerID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractInstanceTags(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
in map[string]*string
|
||||
wantTags map[string]string
|
||||
}{
|
||||
"tags are extracted": {
|
||||
in: map[string]*string{"key": to.StringPtr("value")},
|
||||
wantTags: map[string]string{"key": "value"},
|
||||
},
|
||||
"nil values are skipped": {
|
||||
in: map[string]*string{"key": nil},
|
||||
wantTags: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tags := extractInstanceTags(tc.in)
|
||||
|
||||
assert.Equal(tc.wantTags, tags)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSSHKeys(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
in armcompute.SSHConfiguration
|
||||
wantKeys map[string][]string
|
||||
}{
|
||||
"ssh key is extracted": {
|
||||
in: armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
KeyData: to.StringPtr("key-data"),
|
||||
Path: to.StringPtr("/home/user/.ssh/authorized_keys"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantKeys: map[string][]string{"user": {"key-data"}},
|
||||
},
|
||||
"invalid path is skipped": {
|
||||
in: armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
KeyData: to.StringPtr("key-data"),
|
||||
Path: to.StringPtr("invalid-path"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantKeys: map[string][]string{},
|
||||
},
|
||||
"key data is nil": {
|
||||
in: armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
Path: to.StringPtr("/home/user/.ssh/authorized_keys"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantKeys: map[string][]string{},
|
||||
},
|
||||
"path is nil": {
|
||||
in: armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
KeyData: to.StringPtr("key-data"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantKeys: map[string][]string{},
|
||||
},
|
||||
"public keys are nil": {
|
||||
in: armcompute.SSHConfiguration{},
|
||||
wantKeys: map[string][]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
keys := extractSSHKeys(tc.in)
|
||||
|
||||
assert.Equal(tc.wantKeys, keys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newIMDSStub() *stubIMDSAPI {
|
||||
return &stubIMDSAPI{
|
||||
res: metadataResponse{Compute: struct {
|
||||
ResourceID string `json:"resourceId,omitempty"`
|
||||
}{"/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name"}},
|
||||
}
|
||||
}
|
||||
|
||||
func newScaleSetIMDSStub() *stubIMDSAPI {
|
||||
return &stubIMDSAPI{
|
||||
res: metadataResponse{Compute: struct {
|
||||
ResourceID string `json:"resourceId,omitempty"`
|
||||
}{"/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"}},
|
||||
}
|
||||
}
|
||||
|
||||
func newInvalidIMDSStub() *stubIMDSAPI {
|
||||
return &stubIMDSAPI{
|
||||
res: metadataResponse{Compute: struct {
|
||||
ResourceID string `json:"resourceId,omitempty"`
|
||||
}{"invalid-resource-id"}},
|
||||
}
|
||||
}
|
||||
|
||||
func newNetworkInterfacesStub() *stubNetworkInterfacesAPI {
|
||||
return &stubNetworkInterfacesAPI{
|
||||
getInterface: armnetwork.Interface{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newScaleSetsStub() *stubScaleSetsAPI {
|
||||
return &stubScaleSetsAPI{
|
||||
listPages: [][]*armcompute.VirtualMachineScaleSet{
|
||||
{
|
||||
&armcompute.VirtualMachineScaleSet{
|
||||
Name: to.StringPtr("scale-set-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newVirtualMachineScaleSetsVMsStub() *stubVirtualMachineScaleSetVMsAPI {
|
||||
return &stubVirtualMachineScaleSetVMsAPI{
|
||||
getVM: armcompute.VirtualMachineScaleSetVM{
|
||||
Name: to.StringPtr("scale-set-name_instance-id"),
|
||||
InstanceID: to.StringPtr("instance-id"),
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
|
||||
Properties: &armcompute.VirtualMachineScaleSetVMProperties{
|
||||
NetworkProfile: &armcompute.NetworkProfile{
|
||||
NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
OSProfile: &armcompute.OSProfile{
|
||||
ComputerName: to.StringPtr("scale-set-name-instance-id"),
|
||||
LinuxConfiguration: &armcompute.LinuxConfiguration{
|
||||
SSH: &armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
KeyData: to.StringPtr("key-data"),
|
||||
Path: to.StringPtr("/home/user/.ssh/authorized_keys"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
listPages: [][]*armcompute.VirtualMachineScaleSetVM{
|
||||
{
|
||||
&armcompute.VirtualMachineScaleSetVM{
|
||||
Name: to.StringPtr("scale-set-name_instance-id"),
|
||||
InstanceID: to.StringPtr("instance-id"),
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
|
||||
Properties: &armcompute.VirtualMachineScaleSetVMProperties{
|
||||
NetworkProfile: &armcompute.NetworkProfile{
|
||||
NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
OSProfile: &armcompute.OSProfile{
|
||||
ComputerName: to.StringPtr("scale-set-name-instance-id"),
|
||||
LinuxConfiguration: &armcompute.LinuxConfiguration{
|
||||
SSH: &armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
KeyData: to.StringPtr("key-data"),
|
||||
Path: to.StringPtr("/home/user/.ssh/authorized_keys"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newFailingListsVirtualMachineScaleSetsVMsStub() *stubVirtualMachineScaleSetVMsAPI {
|
||||
return &stubVirtualMachineScaleSetVMsAPI{
|
||||
listPages: [][]*armcompute.VirtualMachineScaleSetVM{
|
||||
{
|
||||
{
|
||||
InstanceID: to.StringPtr("invalid-instance-id"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTagsStub() *stubTagsAPI {
|
||||
return &stubTagsAPI{}
|
||||
}
|
||||
111
bootstrapper/cloudprovider/azure/network.go
Normal file
111
bootstrapper/cloudprovider/azure/network.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
)
|
||||
|
||||
// getVMInterfaces retrieves all network interfaces referenced by a virtual machine.
|
||||
func (m *Metadata) getVMInterfaces(ctx context.Context, vm armcompute.VirtualMachine, resourceGroup string) ([]armnetwork.Interface, error) {
|
||||
if vm.Properties == nil || vm.Properties.NetworkProfile == nil {
|
||||
return []armnetwork.Interface{}, nil
|
||||
}
|
||||
interfaceNames := extractInterfaceNamesFromInterfaceReferences(vm.Properties.NetworkProfile.NetworkInterfaces)
|
||||
networkInterfaces := []armnetwork.Interface{}
|
||||
for _, interfaceName := range interfaceNames {
|
||||
networkInterfacesResp, err := m.networkInterfacesAPI.Get(ctx, resourceGroup, interfaceName, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving network interface %v: %w", interfaceName, err)
|
||||
}
|
||||
networkInterfaces = append(networkInterfaces, networkInterfacesResp.Interface)
|
||||
}
|
||||
return networkInterfaces, nil
|
||||
}
|
||||
|
||||
// getScaleSetVMInterfaces retrieves all network interfaces referenced by a scale set virtual machine.
|
||||
func (m *Metadata) getScaleSetVMInterfaces(ctx context.Context, vm armcompute.VirtualMachineScaleSetVM, resourceGroup, scaleSet, instanceID string) ([]armnetwork.Interface, error) {
|
||||
if vm.Properties == nil || vm.Properties.NetworkProfile == nil {
|
||||
return []armnetwork.Interface{}, nil
|
||||
}
|
||||
interfaceNames := extractInterfaceNamesFromInterfaceReferences(vm.Properties.NetworkProfile.NetworkInterfaces)
|
||||
networkInterfaces := []armnetwork.Interface{}
|
||||
for _, interfaceName := range interfaceNames {
|
||||
networkInterfacesResp, err := m.networkInterfacesAPI.GetVirtualMachineScaleSetNetworkInterface(ctx, resourceGroup, scaleSet, instanceID, interfaceName, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving network interface %v: %w", interfaceName, err)
|
||||
}
|
||||
networkInterfaces = append(networkInterfaces, networkInterfacesResp.Interface)
|
||||
}
|
||||
return networkInterfaces, nil
|
||||
}
|
||||
|
||||
// getScaleSetVMPublicIPAddresses retrieves all public IP addresses from a network interface which is referenced by a scale set virtual machine.
|
||||
func (m *Metadata) getScaleSetVMPublicIPAddresses(ctx context.Context, resourceGroup, scaleSet, instanceID string,
|
||||
networkInterfaces []armnetwork.Interface,
|
||||
) ([]string, error) {
|
||||
var publicIPAddresses []string
|
||||
for _, networkInterface := range networkInterfaces {
|
||||
if networkInterface.Properties == nil || networkInterface.Name == nil {
|
||||
continue
|
||||
}
|
||||
for _, config := range networkInterface.Properties.IPConfigurations {
|
||||
if config == nil || config.Properties == nil || config.Properties.PublicIPAddress == nil || config.Name == nil {
|
||||
continue
|
||||
}
|
||||
publicIPAddressName := *config.Properties.PublicIPAddress.ID
|
||||
publicIPAddressNameParts := strings.Split(publicIPAddressName, "/")
|
||||
publicIPAddressName = publicIPAddressNameParts[len(publicIPAddressNameParts)-1]
|
||||
publicIPAddress, err := m.publicIPAddressesAPI.GetVirtualMachineScaleSetPublicIPAddress(ctx, resourceGroup, scaleSet, instanceID, *networkInterface.Name, *config.Name, publicIPAddressName, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve public ip address %v: %w", publicIPAddressName, err)
|
||||
}
|
||||
if publicIPAddress.Properties == nil || publicIPAddress.Properties.IPAddress == nil {
|
||||
return nil, errors.New("retrieved public ip address has invalid ip address")
|
||||
}
|
||||
publicIPAddresses = append(publicIPAddresses, *publicIPAddress.Properties.IPAddress)
|
||||
}
|
||||
}
|
||||
return publicIPAddresses, nil
|
||||
}
|
||||
|
||||
// extractPrivateIPs extracts private IPs from a list of network interface IP configurations.
|
||||
func extractPrivateIPs(networkInterfaces []armnetwork.Interface) []string {
|
||||
addresses := []string{}
|
||||
for _, networkInterface := range networkInterfaces {
|
||||
if networkInterface.Properties == nil || len(networkInterface.Properties.IPConfigurations) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, config := range networkInterface.Properties.IPConfigurations {
|
||||
if config == nil || config.Properties == nil || config.Properties.PrivateIPAddress == nil {
|
||||
continue
|
||||
}
|
||||
addresses = append(addresses, *config.Properties.PrivateIPAddress)
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
// extractInterfaceNamesFromInterfaceReferences extracts the name of a network interface from a reference id.
|
||||
// Format:
|
||||
// - "/subscriptions/<subscription>/resourceGroups/<resource-group>/providers/Microsoft.Network/networkInterfaces/<interface-name>"
|
||||
// - "/subscriptions/<subscription>/resourceGroups/<resource-group>/providers/Microsoft.Compute/virtualMachineScaleSets/<scale-set-name>/virtualMachines/<instanceID>/networkInterfaces/<interface-name>".
|
||||
func extractInterfaceNamesFromInterfaceReferences(references []*armcompute.NetworkInterfaceReference) []string {
|
||||
interfaceNames := []string{}
|
||||
for _, interfaceReference := range references {
|
||||
if interfaceReference == nil || interfaceReference.ID == nil {
|
||||
continue
|
||||
}
|
||||
interfaceIDParts := strings.Split(*interfaceReference.ID, "/")
|
||||
if len(interfaceIDParts) < 1 {
|
||||
continue
|
||||
}
|
||||
interfaceName := interfaceIDParts[len(interfaceIDParts)-1]
|
||||
interfaceNames = append(interfaceNames, interfaceName)
|
||||
}
|
||||
return interfaceNames
|
||||
}
|
||||
399
bootstrapper/cloudprovider/azure/network_test.go
Normal file
399
bootstrapper/cloudprovider/azure/network_test.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetVMInterfaces(t *testing.T) {
|
||||
wantNetworkInterfaces := []armnetwork.Interface{
|
||||
{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
vm := armcompute.VirtualMachine{
|
||||
Properties: &armcompute.VirtualMachineProperties{
|
||||
NetworkProfile: &armcompute.NetworkProfile{
|
||||
NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
vm armcompute.VirtualMachine
|
||||
networkInterfacesAPI networkInterfacesAPI
|
||||
wantErr bool
|
||||
wantNetworkInterfaces []armnetwork.Interface
|
||||
}{
|
||||
"retrieval works": {
|
||||
vm: vm,
|
||||
networkInterfacesAPI: &stubNetworkInterfacesAPI{
|
||||
getInterface: armnetwork.Interface{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantNetworkInterfaces: wantNetworkInterfaces,
|
||||
},
|
||||
"vm can have 0 interfaces": {
|
||||
vm: armcompute.VirtualMachine{},
|
||||
networkInterfacesAPI: &stubNetworkInterfacesAPI{
|
||||
getInterface: armnetwork.Interface{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantNetworkInterfaces: []armnetwork.Interface{},
|
||||
},
|
||||
"interface retrieval fails": {
|
||||
vm: vm,
|
||||
networkInterfacesAPI: &stubNetworkInterfacesAPI{
|
||||
getErr: errors.New("get err"),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
networkInterfacesAPI: tc.networkInterfacesAPI,
|
||||
}
|
||||
vmNetworkInteraces, err := metadata.getVMInterfaces(context.Background(), tc.vm, "resource-group")
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantNetworkInterfaces, vmNetworkInteraces)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetScaleSetVMInterfaces(t *testing.T) {
|
||||
wantNetworkInterfaces := []armnetwork.Interface{
|
||||
{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
vm := armcompute.VirtualMachineScaleSetVM{
|
||||
Properties: &armcompute.VirtualMachineScaleSetVMProperties{
|
||||
NetworkProfile: &armcompute.NetworkProfile{
|
||||
NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
vm armcompute.VirtualMachineScaleSetVM
|
||||
networkInterfacesAPI networkInterfacesAPI
|
||||
wantErr bool
|
||||
wantNetworkInterfaces []armnetwork.Interface
|
||||
}{
|
||||
"retrieval works": {
|
||||
vm: vm,
|
||||
networkInterfacesAPI: &stubNetworkInterfacesAPI{
|
||||
getInterface: armnetwork.Interface{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantNetworkInterfaces: wantNetworkInterfaces,
|
||||
},
|
||||
"vm can have 0 interfaces": {
|
||||
vm: armcompute.VirtualMachineScaleSetVM{},
|
||||
networkInterfacesAPI: &stubNetworkInterfacesAPI{
|
||||
getInterface: armnetwork.Interface{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantNetworkInterfaces: []armnetwork.Interface{},
|
||||
},
|
||||
"interface retrieval fails": {
|
||||
vm: vm,
|
||||
networkInterfacesAPI: &stubNetworkInterfacesAPI{
|
||||
getErr: errors.New("get err"),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
networkInterfacesAPI: tc.networkInterfacesAPI,
|
||||
}
|
||||
configs, err := metadata.getScaleSetVMInterfaces(context.Background(), tc.vm, "resource-group", "scale-set-name", "instance-id")
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantNetworkInterfaces, configs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetScaleSetVMPublicIPAddresses(t *testing.T) {
|
||||
someErr := errors.New("some err")
|
||||
newNetworkInterfaces := func() []armnetwork.Interface {
|
||||
return []armnetwork.Interface{{
|
||||
Name: to.StringPtr("interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Name: to.StringPtr("ip-config-name"),
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PublicIPAddress: &armnetwork.PublicIPAddress{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/publicIPAddresses/public-ip-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: to.StringPtr("interface-name2"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Name: to.StringPtr("ip-config-name2"),
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PublicIPAddress: &armnetwork.PublicIPAddress{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/publicIPAddresses/public-ip-name2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
networkInterfacesMutator func(*[]armnetwork.Interface)
|
||||
networkInterfaces []armnetwork.Interface
|
||||
publicIPAddressesAPI publicIPAddressesAPI
|
||||
wantIPs []string
|
||||
wantErr bool
|
||||
}{
|
||||
"retrieval works": {
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getVirtualMachineScaleSetPublicIPAddressResponse: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse{
|
||||
PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResult: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResult{
|
||||
PublicIPAddress: armnetwork.PublicIPAddress{
|
||||
Properties: &armnetwork.PublicIPAddressPropertiesFormat{
|
||||
IPAddress: to.StringPtr("192.0.2.1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
networkInterfaces: newNetworkInterfaces(),
|
||||
wantIPs: []string{"192.0.2.1", "192.0.2.1"},
|
||||
},
|
||||
"retrieval works for no valid interfaces": {
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getVirtualMachineScaleSetPublicIPAddressResponse: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse{
|
||||
PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResult: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResult{
|
||||
PublicIPAddress: armnetwork.PublicIPAddress{
|
||||
Properties: &armnetwork.PublicIPAddressPropertiesFormat{
|
||||
IPAddress: to.StringPtr("192.0.2.1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
networkInterfaces: newNetworkInterfaces(),
|
||||
networkInterfacesMutator: func(nets *[]armnetwork.Interface) {
|
||||
(*nets)[0].Properties.IPConfigurations = []*armnetwork.InterfaceIPConfiguration{nil}
|
||||
(*nets)[1] = armnetwork.Interface{Name: nil}
|
||||
},
|
||||
},
|
||||
"fail to get public IP": {
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getErr: someErr},
|
||||
networkInterfaces: newNetworkInterfaces(),
|
||||
wantErr: true,
|
||||
},
|
||||
"fail to parse IPv4 address of public IP": {
|
||||
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getVirtualMachineScaleSetPublicIPAddressResponse: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse{
|
||||
PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResult: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResult{
|
||||
PublicIPAddress: armnetwork.PublicIPAddress{},
|
||||
},
|
||||
}},
|
||||
networkInterfaces: newNetworkInterfaces(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
if tc.networkInterfacesMutator != nil {
|
||||
tc.networkInterfacesMutator(&tc.networkInterfaces)
|
||||
}
|
||||
|
||||
metadata := Metadata{
|
||||
publicIPAddressesAPI: tc.publicIPAddressesAPI,
|
||||
}
|
||||
|
||||
ips, err := metadata.getScaleSetVMPublicIPAddresses(context.Background(), "resource-group", "scale-set-name", "instance-id", tc.networkInterfaces)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantIPs, ips)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPrivateIPs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
networkInterfaces []armnetwork.Interface
|
||||
wantIPs []string
|
||||
}{
|
||||
"extraction works": {
|
||||
networkInterfaces: []armnetwork.Interface{
|
||||
{
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
"can be empty": {
|
||||
networkInterfaces: []armnetwork.Interface{},
|
||||
},
|
||||
"invalid interface is skipped": {
|
||||
networkInterfaces: []armnetwork.Interface{{}},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ips := extractPrivateIPs(tc.networkInterfaces)
|
||||
|
||||
assert.ElementsMatch(tc.wantIPs, ips)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractInterfaceNamesFromInterfaceReferences(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
references []*armcompute.NetworkInterfaceReference
|
||||
wantNames []string
|
||||
}{
|
||||
"extraction with individual interface reference works": {
|
||||
references: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
wantNames: []string{"interface-name"},
|
||||
},
|
||||
"extraction with scale set interface reference works": {
|
||||
references: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
wantNames: []string{"interface-name"},
|
||||
},
|
||||
"can be empty": {
|
||||
references: []*armcompute.NetworkInterfaceReference{},
|
||||
},
|
||||
"interface reference containing nil fields is skipped": {
|
||||
references: []*armcompute.NetworkInterfaceReference{
|
||||
{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
names := extractInterfaceNamesFromInterfaceReferences(tc.references)
|
||||
|
||||
assert.ElementsMatch(tc.wantNames, names)
|
||||
})
|
||||
}
|
||||
}
|
||||
106
bootstrapper/cloudprovider/azure/scaleset.go
Normal file
106
bootstrapper/cloudprovider/azure/scaleset.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
"github.com/edgelesssys/constellation/internal/azureshared"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
controlPlaneScaleSetRegexp = regexp.MustCompile(`constellation-scale-set-controlplanes-[0-9a-zA-Z]+$`)
|
||||
workerScaleSetRegexp = regexp.MustCompile(`constellation-scale-set-workers-[0-9a-zA-Z]+$`)
|
||||
)
|
||||
|
||||
// getScaleSetVM tries to get an azure vm belonging to a scale set.
|
||||
func (m *Metadata) getScaleSetVM(ctx context.Context, providerID string) (metadata.InstanceMetadata, error) {
|
||||
_, resourceGroup, scaleSet, instanceID, err := azureshared.ScaleSetInformationFromProviderID(providerID)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
vmResp, err := m.virtualMachineScaleSetVMsAPI.Get(ctx, resourceGroup, scaleSet, instanceID, nil)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
networkInterfaces, err := m.getScaleSetVMInterfaces(ctx, vmResp.VirtualMachineScaleSetVM, resourceGroup, scaleSet, instanceID)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
publicIPAddresses, err := m.getScaleSetVMPublicIPAddresses(ctx, resourceGroup, scaleSet, instanceID, networkInterfaces)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
|
||||
return convertScaleSetVMToCoreInstance(scaleSet, vmResp.VirtualMachineScaleSetVM, networkInterfaces, publicIPAddresses)
|
||||
}
|
||||
|
||||
// listScaleSetVMs lists all scale set VMs in the current resource group.
|
||||
func (m *Metadata) listScaleSetVMs(ctx context.Context, resourceGroup string) ([]metadata.InstanceMetadata, error) {
|
||||
instances := []metadata.InstanceMetadata{}
|
||||
scaleSetPager := m.scaleSetsAPI.List(resourceGroup, nil)
|
||||
for scaleSetPager.NextPage(ctx) {
|
||||
for _, scaleSet := range scaleSetPager.PageResponse().Value {
|
||||
if scaleSet == nil || scaleSet.Name == nil {
|
||||
continue
|
||||
}
|
||||
vmPager := m.virtualMachineScaleSetVMsAPI.List(resourceGroup, *scaleSet.Name, nil)
|
||||
for vmPager.NextPage(ctx) {
|
||||
for _, vm := range vmPager.PageResponse().Value {
|
||||
if vm == nil || vm.InstanceID == nil {
|
||||
continue
|
||||
}
|
||||
interfaces, err := m.getScaleSetVMInterfaces(ctx, *vm, resourceGroup, *scaleSet.Name, *vm.InstanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instance, err := convertScaleSetVMToCoreInstance(*scaleSet.Name, *vm, interfaces, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// convertScaleSetVMToCoreInstance converts an azure scale set virtual machine with interface configurations into a core.Instance.
|
||||
func convertScaleSetVMToCoreInstance(scaleSet string, vm armcompute.VirtualMachineScaleSetVM, networkInterfaces []armnetwork.Interface, publicIPAddresses []string) (metadata.InstanceMetadata, error) {
|
||||
if vm.ID == nil {
|
||||
return metadata.InstanceMetadata{}, errors.New("retrieving instance from armcompute API client returned no instance ID")
|
||||
}
|
||||
if vm.Properties == nil || vm.Properties.OSProfile == nil || vm.Properties.OSProfile.ComputerName == nil {
|
||||
return metadata.InstanceMetadata{}, errors.New("retrieving instance from armcompute API client returned no computer name")
|
||||
}
|
||||
var sshKeys map[string][]string
|
||||
if vm.Properties.OSProfile.LinuxConfiguration == nil || vm.Properties.OSProfile.LinuxConfiguration.SSH == nil {
|
||||
sshKeys = map[string][]string{}
|
||||
} else {
|
||||
sshKeys = extractSSHKeys(*vm.Properties.OSProfile.LinuxConfiguration.SSH)
|
||||
}
|
||||
return metadata.InstanceMetadata{
|
||||
Name: *vm.Properties.OSProfile.ComputerName,
|
||||
ProviderID: "azure://" + *vm.ID,
|
||||
Role: extractScaleSetVMRole(scaleSet),
|
||||
PrivateIPs: extractPrivateIPs(networkInterfaces),
|
||||
PublicIPs: publicIPAddresses,
|
||||
SSHKeys: sshKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractScaleSetVMRole extracts the constellation role of a scale set using its name.
|
||||
func extractScaleSetVMRole(scaleSet string) role.Role {
|
||||
if controlPlaneScaleSetRegexp.MatchString(scaleSet) {
|
||||
return role.ControlPlane
|
||||
}
|
||||
if workerScaleSetRegexp.MatchString(scaleSet) {
|
||||
return role.Worker
|
||||
}
|
||||
return role.Unknown
|
||||
}
|
||||
335
bootstrapper/cloudprovider/azure/scaleset_test.go
Normal file
335
bootstrapper/cloudprovider/azure/scaleset_test.go
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetScaleSetVM(t *testing.T) {
|
||||
wantInstance := metadata.InstanceMetadata{
|
||||
Name: "scale-set-name-instance-id",
|
||||
ProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
SSHKeys: map[string][]string{"user": {"key-data"}},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
networkInterfacesAPI networkInterfacesAPI
|
||||
virtualMachineScaleSetVMsAPI virtualMachineScaleSetVMsAPI
|
||||
wantErr bool
|
||||
wantInstance metadata.InstanceMetadata
|
||||
}{
|
||||
"getVM for scale set instance works": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
|
||||
wantInstance: wantInstance,
|
||||
},
|
||||
"getVM for individual instance must fail": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/instance-name",
|
||||
wantErr: true,
|
||||
},
|
||||
"Get fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
virtualMachineScaleSetVMsAPI: newFailingGetScaleSetVirtualMachinesStub(),
|
||||
wantErr: true,
|
||||
},
|
||||
"conversion fails": {
|
||||
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
virtualMachineScaleSetVMsAPI: newGetInvalidScaleSetVirtualMachinesStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
networkInterfacesAPI: tc.networkInterfacesAPI,
|
||||
virtualMachineScaleSetVMsAPI: tc.virtualMachineScaleSetVMsAPI,
|
||||
}
|
||||
instance, err := metadata.getScaleSetVM(context.Background(), tc.providerID)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantInstance, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListScaleSetVMs(t *testing.T) {
|
||||
wantInstances := []metadata.InstanceMetadata{
|
||||
{
|
||||
Name: "scale-set-name-instance-id",
|
||||
ProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
SSHKeys: map[string][]string{"user": {"key-data"}},
|
||||
},
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
imdsAPI imdsAPI
|
||||
networkInterfacesAPI networkInterfacesAPI
|
||||
virtualMachineScaleSetVMsAPI virtualMachineScaleSetVMsAPI
|
||||
scaleSetsAPI scaleSetsAPI
|
||||
wantErr bool
|
||||
wantInstances []metadata.InstanceMetadata
|
||||
}{
|
||||
"listVMs works": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
|
||||
scaleSetsAPI: newScaleSetsStub(),
|
||||
wantInstances: wantInstances,
|
||||
},
|
||||
"invalid scale sets are skipped": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
|
||||
scaleSetsAPI: newListContainingNilScaleSetStub(),
|
||||
wantInstances: wantInstances,
|
||||
},
|
||||
"listVMs can return 0 VMs": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: &stubVirtualMachineScaleSetVMsAPI{},
|
||||
scaleSetsAPI: newScaleSetsStub(),
|
||||
wantInstances: []metadata.InstanceMetadata{},
|
||||
},
|
||||
"can skip nil in VM list": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newListContainingNilScaleSetVirtualMachinesStub(),
|
||||
scaleSetsAPI: newScaleSetsStub(),
|
||||
wantInstances: wantInstances,
|
||||
},
|
||||
"converting instance fails": {
|
||||
imdsAPI: newIMDSStub(),
|
||||
networkInterfacesAPI: newNetworkInterfacesStub(),
|
||||
virtualMachineScaleSetVMsAPI: newListContainingInvalidScaleSetVirtualMachinesStub(),
|
||||
scaleSetsAPI: newScaleSetsStub(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
metadata := Metadata{
|
||||
imdsAPI: tc.imdsAPI,
|
||||
networkInterfacesAPI: tc.networkInterfacesAPI,
|
||||
virtualMachineScaleSetVMsAPI: tc.virtualMachineScaleSetVMsAPI,
|
||||
scaleSetsAPI: tc.scaleSetsAPI,
|
||||
}
|
||||
instances, err := metadata.listScaleSetVMs(context.Background(), "resource-group")
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.ElementsMatch(tc.wantInstances, instances)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertScaleSetVMToCoreInstance(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
inVM armcompute.VirtualMachineScaleSetVM
|
||||
inInterface []armnetwork.Interface
|
||||
inPublicIPs []string
|
||||
wantErr bool
|
||||
wantInstance metadata.InstanceMetadata
|
||||
}{
|
||||
"conversion works": {
|
||||
inVM: armcompute.VirtualMachineScaleSetVM{
|
||||
Name: to.StringPtr("scale-set-name_instance-id"),
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
|
||||
Tags: map[string]*string{"tag-key": to.StringPtr("tag-value")},
|
||||
Properties: &armcompute.VirtualMachineScaleSetVMProperties{
|
||||
OSProfile: &armcompute.OSProfile{
|
||||
ComputerName: to.StringPtr("scale-set-name-instance-id"),
|
||||
},
|
||||
},
|
||||
},
|
||||
inInterface: []armnetwork.Interface{
|
||||
{
|
||||
Name: to.StringPtr("scale-set-name_instance-id"),
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/interface-name"),
|
||||
Properties: &armnetwork.InterfacePropertiesFormat{
|
||||
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
|
||||
{
|
||||
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: to.StringPtr("192.0.2.0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inPublicIPs: []string{"192.0.2.100", "192.0.2.101"},
|
||||
wantInstance: metadata.InstanceMetadata{
|
||||
Name: "scale-set-name-instance-id",
|
||||
ProviderID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
PublicIPs: []string{"192.0.2.100", "192.0.2.101"},
|
||||
SSHKeys: map[string][]string{},
|
||||
},
|
||||
},
|
||||
"invalid instance": {
|
||||
inVM: armcompute.VirtualMachineScaleSetVM{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
instance, err := convertScaleSetVMToCoreInstance("scale-set", tc.inVM, tc.inInterface, tc.inPublicIPs)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantInstance, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractScaleSetVMRole(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
scaleSet string
|
||||
wantRole role.Role
|
||||
}{
|
||||
"bootstrapper role": {
|
||||
scaleSet: "constellation-scale-set-bootstrappers-abcd123",
|
||||
wantRole: role.ControlPlane,
|
||||
},
|
||||
"node role": {
|
||||
scaleSet: "constellation-scale-set-nodes-abcd123",
|
||||
wantRole: role.Worker,
|
||||
},
|
||||
"unknown role": {
|
||||
scaleSet: "unknown",
|
||||
wantRole: role.Unknown,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
role := extractScaleSetVMRole(tc.scaleSet)
|
||||
|
||||
assert.Equal(tc.wantRole, role)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newFailingGetScaleSetVirtualMachinesStub() *stubVirtualMachineScaleSetVMsAPI {
|
||||
return &stubVirtualMachineScaleSetVMsAPI{
|
||||
getErr: errors.New("get err"),
|
||||
}
|
||||
}
|
||||
|
||||
func newGetInvalidScaleSetVirtualMachinesStub() *stubVirtualMachineScaleSetVMsAPI {
|
||||
return &stubVirtualMachineScaleSetVMsAPI{
|
||||
getVM: armcompute.VirtualMachineScaleSetVM{},
|
||||
}
|
||||
}
|
||||
|
||||
func newListContainingNilScaleSetVirtualMachinesStub() *stubVirtualMachineScaleSetVMsAPI {
|
||||
return &stubVirtualMachineScaleSetVMsAPI{
|
||||
listPages: [][]*armcompute.VirtualMachineScaleSetVM{
|
||||
{
|
||||
nil,
|
||||
{
|
||||
Name: to.StringPtr("scale-set-name_instance-id"),
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
|
||||
InstanceID: to.StringPtr("instance-id"),
|
||||
Tags: map[string]*string{
|
||||
"tag-key": to.StringPtr("tag-value"),
|
||||
},
|
||||
Properties: &armcompute.VirtualMachineScaleSetVMProperties{
|
||||
NetworkProfile: &armcompute.NetworkProfile{
|
||||
NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
OSProfile: &armcompute.OSProfile{
|
||||
ComputerName: to.StringPtr("scale-set-name-instance-id"),
|
||||
LinuxConfiguration: &armcompute.LinuxConfiguration{
|
||||
SSH: &armcompute.SSHConfiguration{
|
||||
PublicKeys: []*armcompute.SSHPublicKey{
|
||||
{
|
||||
KeyData: to.StringPtr("key-data"),
|
||||
Path: to.StringPtr("/home/user/.ssh/authorized_keys"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newListContainingInvalidScaleSetVirtualMachinesStub() *stubVirtualMachineScaleSetVMsAPI {
|
||||
return &stubVirtualMachineScaleSetVMsAPI{
|
||||
listPages: [][]*armcompute.VirtualMachineScaleSetVM{
|
||||
{
|
||||
{
|
||||
Name: nil,
|
||||
ID: nil,
|
||||
InstanceID: to.StringPtr("instance-id"),
|
||||
Properties: &armcompute.VirtualMachineScaleSetVMProperties{
|
||||
OSProfile: &armcompute.OSProfile{
|
||||
ComputerName: nil,
|
||||
},
|
||||
NetworkProfile: &armcompute.NetworkProfile{
|
||||
NetworkInterfaces: []*armcompute.NetworkInterfaceReference{
|
||||
{
|
||||
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newListContainingNilScaleSetStub() *stubScaleSetsAPI {
|
||||
return &stubScaleSetsAPI{
|
||||
listPages: [][]*armcompute.VirtualMachineScaleSet{
|
||||
{
|
||||
nil,
|
||||
{Name: to.StringPtr("scale-set-name")},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
19
bootstrapper/cloudprovider/azure/securityGroup.go
Normal file
19
bootstrapper/cloudprovider/azure/securityGroup.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
)
|
||||
|
||||
// getNetworkSecurityGroup retrieves the list of security groups for the given resource group.
|
||||
func (m *Metadata) getNetworkSecurityGroup(ctx context.Context, resourceGroup string) (*armnetwork.SecurityGroup, error) {
|
||||
pager := m.securityGroupsAPI.List(resourceGroup, nil)
|
||||
for pager.NextPage(ctx) {
|
||||
for _, securityGroup := range pager.PageResponse().Value {
|
||||
return securityGroup, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no security group found for resource group %q", resourceGroup)
|
||||
}
|
||||
21
bootstrapper/cloudprovider/azure/virtualnetwork.go
Normal file
21
bootstrapper/cloudprovider/azure/virtualnetwork.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
)
|
||||
|
||||
// getVirtualNetwork return the first virtual network found in the resource group.
|
||||
func (m *Metadata) getVirtualNetwork(ctx context.Context, resourceGroup string) (*armnetwork.VirtualNetwork, error) {
|
||||
pager := m.virtualNetworksAPI.List(resourceGroup, nil)
|
||||
for pager.NextPage(ctx) {
|
||||
for _, network := range pager.PageResponse().Value {
|
||||
if network != nil {
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no virtual network found in resource group %s", resourceGroup)
|
||||
}
|
||||
109
bootstrapper/cloudprovider/azure/wrappers.go
Normal file
109
bootstrapper/cloudprovider/azure/wrappers.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
|
||||
)
|
||||
|
||||
type virtualNetworksClient struct {
|
||||
*armnetwork.VirtualNetworksClient
|
||||
}
|
||||
|
||||
func (c *virtualNetworksClient) List(resourceGroupName string, options *armnetwork.VirtualNetworksClientListOptions) virtualNetworksClientListPager {
|
||||
return c.VirtualNetworksClient.List(resourceGroupName, options)
|
||||
}
|
||||
|
||||
type securityGroupsClient struct {
|
||||
*armnetwork.SecurityGroupsClient
|
||||
}
|
||||
|
||||
func (c *securityGroupsClient) List(resourceGroupName string, options *armnetwork.SecurityGroupsClientListOptions) securityGroupsClientListPager {
|
||||
return c.SecurityGroupsClient.List(resourceGroupName, options)
|
||||
}
|
||||
|
||||
type networkInterfacesClient struct {
|
||||
*armnetwork.InterfacesClient
|
||||
}
|
||||
|
||||
func (c *networkInterfacesClient) GetVirtualMachineScaleSetNetworkInterface(ctx context.Context, resourceGroupName string,
|
||||
virtualMachineScaleSetName string, virtualmachineIndex string, networkInterfaceName string,
|
||||
options *armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceOptions,
|
||||
) (armnetwork.InterfacesClientGetVirtualMachineScaleSetNetworkInterfaceResponse, error) {
|
||||
return c.InterfacesClient.GetVirtualMachineScaleSetNetworkInterface(ctx, resourceGroupName, virtualMachineScaleSetName, virtualmachineIndex, networkInterfaceName, options)
|
||||
}
|
||||
|
||||
func (c *networkInterfacesClient) Get(ctx context.Context, resourceGroupName string, networkInterfaceName string,
|
||||
options *armnetwork.InterfacesClientGetOptions,
|
||||
) (armnetwork.InterfacesClientGetResponse, error) {
|
||||
return c.InterfacesClient.Get(ctx, resourceGroupName, networkInterfaceName, options)
|
||||
}
|
||||
|
||||
type publicIPAddressesClient struct {
|
||||
*armnetwork.PublicIPAddressesClient
|
||||
}
|
||||
|
||||
func (c *publicIPAddressesClient) GetVirtualMachineScaleSetPublicIPAddress(ctx context.Context, resourceGroupName string,
|
||||
virtualMachineScaleSetName string, virtualmachineIndex string, networkInterfaceName string,
|
||||
ipConfigurationName string, publicIPAddressName string,
|
||||
options *armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressOptions,
|
||||
) (armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse, error) {
|
||||
return c.PublicIPAddressesClient.GetVirtualMachineScaleSetPublicIPAddress(ctx, resourceGroupName, virtualMachineScaleSetName, virtualmachineIndex, networkInterfaceName, ipConfigurationName, publicIPAddressName, options)
|
||||
}
|
||||
|
||||
func (c *publicIPAddressesClient) Get(ctx context.Context, resourceGroupName string, publicIPAddressName string,
|
||||
options *armnetwork.PublicIPAddressesClientGetOptions,
|
||||
) (armnetwork.PublicIPAddressesClientGetResponse, error) {
|
||||
return c.PublicIPAddressesClient.Get(ctx, resourceGroupName, publicIPAddressName, options)
|
||||
}
|
||||
|
||||
type loadBalancersClient struct {
|
||||
*armnetwork.LoadBalancersClient
|
||||
}
|
||||
|
||||
func (c *loadBalancersClient) List(resourceGroupName string, options *armnetwork.LoadBalancersClientListOptions) loadBalancersClientListPager {
|
||||
return c.LoadBalancersClient.List(resourceGroupName, options)
|
||||
}
|
||||
|
||||
type virtualMachineScaleSetVMsClient struct {
|
||||
*armcompute.VirtualMachineScaleSetVMsClient
|
||||
}
|
||||
|
||||
func (c *virtualMachineScaleSetVMsClient) Get(ctx context.Context, resourceGroupName, vmScaleSetName, instanceID string, options *armcompute.VirtualMachineScaleSetVMsClientGetOptions) (armcompute.VirtualMachineScaleSetVMsClientGetResponse, error) {
|
||||
return c.VirtualMachineScaleSetVMsClient.Get(ctx, resourceGroupName, vmScaleSetName, instanceID, options)
|
||||
}
|
||||
|
||||
func (c *virtualMachineScaleSetVMsClient) List(resourceGroupName, virtualMachineScaleSetName string, options *armcompute.VirtualMachineScaleSetVMsClientListOptions) virtualMachineScaleSetVMsClientListPager {
|
||||
return c.VirtualMachineScaleSetVMsClient.List(resourceGroupName, virtualMachineScaleSetName, options)
|
||||
}
|
||||
|
||||
type tagsClient struct {
|
||||
*armresources.TagsClient
|
||||
}
|
||||
|
||||
func (c *tagsClient) CreateOrUpdateAtScope(ctx context.Context, scope string, parameters armresources.TagsResource, options *armresources.TagsClientCreateOrUpdateAtScopeOptions) (armresources.TagsClientCreateOrUpdateAtScopeResponse, error) {
|
||||
return c.TagsClient.CreateOrUpdateAtScope(ctx, scope, parameters, options)
|
||||
}
|
||||
|
||||
func (c *tagsClient) UpdateAtScope(ctx context.Context, scope string, parameters armresources.TagsPatchResource, options *armresources.TagsClientUpdateAtScopeOptions) (armresources.TagsClientUpdateAtScopeResponse, error) {
|
||||
return c.TagsClient.UpdateAtScope(ctx, scope, parameters, options)
|
||||
}
|
||||
|
||||
type scaleSetsClient struct {
|
||||
*armcompute.VirtualMachineScaleSetsClient
|
||||
}
|
||||
|
||||
func (c *scaleSetsClient) List(resourceGroupName string, options *armcompute.VirtualMachineScaleSetsClientListOptions) virtualMachineScaleSetsClientListPager {
|
||||
return c.VirtualMachineScaleSetsClient.List(resourceGroupName, options)
|
||||
}
|
||||
|
||||
type applicationInsightsClient struct {
|
||||
*armapplicationinsights.ComponentsClient
|
||||
}
|
||||
|
||||
func (c *applicationInsightsClient) Get(ctx context.Context, resourceGroupName string, resourceName string, options *armapplicationinsights.ComponentsClientGetOptions) (armapplicationinsights.ComponentsClientGetResponse, error) {
|
||||
return c.ComponentsClient.Get(ctx, resourceGroupName, resourceName, options)
|
||||
}
|
||||
51
bootstrapper/cloudprovider/gcp/api.go
Normal file
51
bootstrapper/cloudprovider/gcp/api.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
)
|
||||
|
||||
type instanceAPI interface {
|
||||
Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error)
|
||||
List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) InstanceIterator
|
||||
SetMetadata(ctx context.Context, req *computepb.SetMetadataInstanceRequest, opts ...gax.CallOption) (*compute.Operation, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type subnetworkAPI interface {
|
||||
List(ctx context.Context, req *computepb.ListSubnetworksRequest, opts ...gax.CallOption) SubnetworkIterator
|
||||
Get(ctx context.Context, req *computepb.GetSubnetworkRequest, opts ...gax.CallOption) (*computepb.Subnetwork, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type forwardingRulesAPI interface {
|
||||
List(ctx context.Context, req *computepb.ListForwardingRulesRequest, opts ...gax.CallOption) ForwardingRuleIterator
|
||||
Close() error
|
||||
}
|
||||
|
||||
type metadataAPI interface {
|
||||
InstanceAttributeValue(attr string) (string, error)
|
||||
ProjectID() (string, error)
|
||||
Zone() (string, error)
|
||||
InstanceName() (string, error)
|
||||
}
|
||||
|
||||
type Operation interface {
|
||||
Proto() *computepb.Operation
|
||||
}
|
||||
|
||||
type InstanceIterator interface {
|
||||
Next() (*computepb.Instance, error)
|
||||
}
|
||||
|
||||
type SubnetworkIterator interface {
|
||||
Next() (*computepb.Subnetwork, error)
|
||||
}
|
||||
|
||||
type ForwardingRuleIterator interface {
|
||||
Next() (*computepb.ForwardingRule, error)
|
||||
}
|
||||
59
bootstrapper/cloudprovider/gcp/autoscaler.go
Normal file
59
bootstrapper/cloudprovider/gcp/autoscaler.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Autoscaler holds the GCP cluster-autoscaler configuration.
|
||||
type Autoscaler struct{}
|
||||
|
||||
// Name returns the cloud-provider name as used by k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Name() string {
|
||||
return "gce"
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Secrets(instance, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
return resources.Secrets{}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{
|
||||
{
|
||||
Name: "gcekey",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
Secret: &k8s.SecretVolumeSource{
|
||||
SecretName: "gcekey",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{
|
||||
{
|
||||
Name: "gcekey",
|
||||
ReadOnly: true,
|
||||
MountPath: "/var/secrets/google",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *Autoscaler) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{
|
||||
{
|
||||
Name: "GOOGLE_APPLICATION_CREDENTIALS",
|
||||
Value: "/var/secrets/google/key.json",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Supported is used to determine if we support autoscaling for the cloud provider.
|
||||
func (a *Autoscaler) Supported() bool {
|
||||
return true
|
||||
}
|
||||
19
bootstrapper/cloudprovider/gcp/autoscaler_test.go
Normal file
19
bootstrapper/cloudprovider/gcp/autoscaler_test.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrivialAutoscalerFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
autoscaler := Autoscaler{}
|
||||
|
||||
assert.NotEmpty(autoscaler.Name())
|
||||
assert.Empty(autoscaler.Secrets("", ""))
|
||||
assert.NotEmpty(autoscaler.Volumes())
|
||||
assert.NotEmpty(autoscaler.VolumeMounts())
|
||||
assert.NotEmpty(autoscaler.Env())
|
||||
assert.True(autoscaler.Supported())
|
||||
}
|
||||
164
bootstrapper/cloudprovider/gcp/ccm.go
Normal file
164
bootstrapper/cloudprovider/gcp/ccm.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/internal/gcpshared"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// CloudControllerManager holds the gcp cloud-controller-manager configuration.
|
||||
type CloudControllerManager struct{}
|
||||
|
||||
// Image returns the container image used to provide cloud-controller-manager for the cloud-provider.
|
||||
func (c *CloudControllerManager) Image() string {
|
||||
return cloudprovider.CloudControllerManagerImageGCP
|
||||
}
|
||||
|
||||
// Path returns the path used by cloud-controller-manager executable within the container image.
|
||||
func (c *CloudControllerManager) Path() string {
|
||||
return "/cloud-controller-manager"
|
||||
}
|
||||
|
||||
// Name returns the cloud-provider name as used by k8s cloud-controller-manager (k8s.gcr.io/cloud-controller-manager).
|
||||
func (c *CloudControllerManager) Name() string {
|
||||
return "gce"
|
||||
}
|
||||
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-controller-manager command.
|
||||
func (c *CloudControllerManager) ExtraArgs() []string {
|
||||
return []string{
|
||||
"--use-service-account-credentials",
|
||||
"--controllers=cloud-node,cloud-node-lifecycle,nodeipam,service,route",
|
||||
"--cloud-config=/etc/gce/gce.conf",
|
||||
"--cidr-allocator-type=CloudAllocator",
|
||||
"--allocate-node-cidrs=true",
|
||||
"--configure-cloud-routes=false",
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigMaps returns a list of ConfigMaps to deploy together with the k8s cloud-controller-manager
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/configmap/ .
|
||||
func (c *CloudControllerManager) ConfigMaps(instance metadata.InstanceMetadata) (resources.ConfigMaps, error) {
|
||||
// GCP CCM expects cloud config to contain the GCP project-id and other configuration.
|
||||
// reference: https://github.com/kubernetes/cloud-provider-gcp/blob/master/cluster/gce/gci/configure-helper.sh#L791-L892
|
||||
var config strings.Builder
|
||||
config.WriteString("[global]\n")
|
||||
projectID, _, _, err := gcpshared.SplitProviderID(instance.ProviderID)
|
||||
if err != nil {
|
||||
return resources.ConfigMaps{}, err
|
||||
}
|
||||
config.WriteString(fmt.Sprintf("project-id = %s\n", projectID))
|
||||
config.WriteString("use-metadata-server = true\n")
|
||||
|
||||
nameParts := strings.Split(instance.Name, "-")
|
||||
config.WriteString("node-tags = constellation-" + nameParts[len(nameParts)-2] + "\n")
|
||||
|
||||
return resources.ConfigMaps{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "gceconf",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"gce.conf": config.String(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/secret/ .
|
||||
func (c *CloudControllerManager) Secrets(ctx context.Context, _ string, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
serviceAccountKey, err := gcpshared.ServiceAccountKeyFromURI(cloudServiceAccountURI)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
rawKey, err := json.Marshal(serviceAccountKey)
|
||||
if err != nil {
|
||||
return resources.Secrets{}, err
|
||||
}
|
||||
|
||||
return resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "gcekey",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key.json": rawKey,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/storage/volumes/ .
|
||||
func (c *CloudControllerManager) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{
|
||||
{
|
||||
Name: "gceconf",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
ConfigMap: &k8s.ConfigMapVolumeSource{
|
||||
LocalObjectReference: k8s.LocalObjectReference{
|
||||
Name: "gceconf",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "gcekey",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
Secret: &k8s.SecretVolumeSource{
|
||||
SecretName: "gcekey",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cloud-controller-manager.
|
||||
func (c *CloudControllerManager) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{
|
||||
{
|
||||
Name: "gceconf",
|
||||
ReadOnly: true,
|
||||
MountPath: "/etc/gce",
|
||||
},
|
||||
{
|
||||
Name: "gcekey",
|
||||
ReadOnly: true,
|
||||
MountPath: "/var/secrets/google",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cloud-controller-manager.
|
||||
func (c *CloudControllerManager) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{
|
||||
{
|
||||
Name: "GOOGLE_APPLICATION_CREDENTIALS",
|
||||
Value: "/var/secrets/google/key.json",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Supported is used to determine if cloud controller manager is implemented for this cloud provider.
|
||||
func (c *CloudControllerManager) Supported() bool {
|
||||
return true
|
||||
}
|
||||
143
bootstrapper/cloudprovider/gcp/ccm_test.go
Normal file
143
bootstrapper/cloudprovider/gcp/ccm_test.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/internal/gcpshared"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestConfigMaps(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
instance metadata.InstanceMetadata
|
||||
wantConfigMaps resources.ConfigMaps
|
||||
wantErr bool
|
||||
}{
|
||||
"ConfigMaps works": {
|
||||
instance: metadata.InstanceMetadata{ProviderID: "gce://project-id/zone/instanceName-UID-0", Name: "instanceName-UID-0"},
|
||||
wantConfigMaps: resources.ConfigMaps{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "gceconf",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"gce.conf": `[global]
|
||||
project-id = project-id
|
||||
use-metadata-server = true
|
||||
node-tags = constellation-UID
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid providerID fails": {
|
||||
instance: metadata.InstanceMetadata{ProviderID: "invalid"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
cloud := CloudControllerManager{}
|
||||
configMaps, err := cloud.ConfigMaps(tc.instance)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantConfigMaps, configMaps)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
serviceAccountKey := gcpshared.ServiceAccountKey{
|
||||
Type: "type",
|
||||
ProjectID: "project-id",
|
||||
PrivateKeyID: "private-key-id",
|
||||
PrivateKey: "private-key",
|
||||
ClientEmail: "client-email",
|
||||
ClientID: "client-id",
|
||||
AuthURI: "auth-uri",
|
||||
TokenURI: "token-uri",
|
||||
AuthProviderX509CertURL: "auth-provider-x509-cert-url",
|
||||
ClientX509CertURL: "client-x509-cert-url",
|
||||
}
|
||||
rawKey, err := json.Marshal(serviceAccountKey)
|
||||
require.NoError(t, err)
|
||||
testCases := map[string]struct {
|
||||
instance metadata.InstanceMetadata
|
||||
cloudServiceAccountURI string
|
||||
wantSecrets resources.Secrets
|
||||
wantErr bool
|
||||
}{
|
||||
"Secrets works": {
|
||||
cloudServiceAccountURI: "serviceaccount://gcp?type=type&project_id=project-id&private_key_id=private-key-id&private_key=private-key&client_email=client-email&client_id=client-id&auth_uri=auth-uri&token_uri=token-uri&auth_provider_x509_cert_url=auth-provider-x509-cert-url&client_x509_cert_url=client-x509-cert-url",
|
||||
wantSecrets: resources.Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "gcekey",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key.json": rawKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid serviceAccountKey fails": {
|
||||
cloudServiceAccountURI: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
cloud := CloudControllerManager{}
|
||||
secrets, err := cloud.Secrets(context.Background(), tc.instance.ProviderID, tc.cloudServiceAccountURI)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantSecrets, secrets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrivialCCMFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cloud := CloudControllerManager{}
|
||||
|
||||
assert.NotEmpty(cloud.Image())
|
||||
assert.NotEmpty(cloud.Path())
|
||||
assert.NotEmpty(cloud.Name())
|
||||
assert.NotEmpty(cloud.ExtraArgs())
|
||||
assert.NotEmpty(cloud.Volumes())
|
||||
assert.NotEmpty(cloud.VolumeMounts())
|
||||
assert.NotEmpty(cloud.Env())
|
||||
assert.True(cloud.Supported())
|
||||
}
|
||||
416
bootstrapper/cloudprovider/gcp/client.go
Normal file
416
bootstrapper/cloudprovider/gcp/client.go
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/internal/gcpshared"
|
||||
"google.golang.org/api/iterator"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
gcpSSHMetadataKey = "ssh-keys"
|
||||
constellationUIDMetadataKey = "constellation-uid"
|
||||
)
|
||||
|
||||
var zoneFromRegionRegex = regexp.MustCompile("([a-z]*-[a-z]*[0-9])")
|
||||
|
||||
// Client implements the gcp.API interface.
|
||||
type Client struct {
|
||||
instanceAPI
|
||||
subnetworkAPI
|
||||
metadataAPI
|
||||
forwardingRulesAPI
|
||||
}
|
||||
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(ctx context.Context) (*Client, error) {
|
||||
insAPI, err := compute.NewInstancesRESTClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subnetAPI, err := compute.NewSubnetworksRESTClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
forwardingRulesAPI, err := compute.NewForwardingRulesRESTClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
instanceAPI: &instanceClient{insAPI},
|
||||
subnetworkAPI: &subnetworkClient{subnetAPI},
|
||||
forwardingRulesAPI: &forwardingRulesClient{forwardingRulesAPI},
|
||||
metadataAPI: &metadataClient{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RetrieveInstances returns list of instances including their ips and metadata.
|
||||
func (c *Client) RetrieveInstances(ctx context.Context, project, zone string) ([]metadata.InstanceMetadata, error) {
|
||||
uid, err := c.uid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &computepb.ListInstancesRequest{
|
||||
Project: project,
|
||||
Zone: zone,
|
||||
}
|
||||
instanceIterator := c.instanceAPI.List(ctx, req)
|
||||
|
||||
instances := []metadata.InstanceMetadata{}
|
||||
for {
|
||||
resp, err := instanceIterator.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving instance list from compute API client: %w", err)
|
||||
}
|
||||
metadata := extractInstanceMetadata(resp.Metadata, "", false)
|
||||
// skip instances not belonging to the current constellation
|
||||
if instanceUID, ok := metadata[constellationUIDMetadataKey]; !ok || instanceUID != uid {
|
||||
continue
|
||||
}
|
||||
instance, err := convertToCoreInstance(resp, project, zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// RetrieveInstance returns a an instance including ips and metadata.
|
||||
func (c *Client) RetrieveInstance(ctx context.Context, project, zone, instanceName string) (metadata.InstanceMetadata, error) {
|
||||
instance, err := c.getComputeInstance(ctx, project, zone, instanceName)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
|
||||
return convertToCoreInstance(instance, project, zone)
|
||||
}
|
||||
|
||||
// RetrieveProjectID retrieves the GCP projectID containing the current instance.
|
||||
func (c *Client) RetrieveProjectID() (string, error) {
|
||||
value, err := c.metadataAPI.ProjectID()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("requesting GCP projectID failed %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// RetrieveZone retrieves the GCP zone containing the current instance.
|
||||
func (c *Client) RetrieveZone() (string, error) {
|
||||
value, err := c.metadataAPI.Zone()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("requesting GCP zone failed %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (c *Client) RetrieveInstanceName() (string, error) {
|
||||
value, err := c.metadataAPI.InstanceName()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("requesting GCP instanceName failed %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (c *Client) RetrieveInstanceMetadata(attr string) (string, error) {
|
||||
value, err := c.metadataAPI.InstanceAttributeValue(attr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("requesting GCP instance metadata: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// SetInstanceMetadata modifies a key value pair of metadata for the instance specified by project, zone and instanceName.
|
||||
func (c *Client) SetInstanceMetadata(ctx context.Context, project, zone, instanceName, key, value string) error {
|
||||
instance, err := c.getComputeInstance(ctx, project, zone, instanceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving instance metadata: %w", err)
|
||||
}
|
||||
if instance == nil || instance.Metadata == nil {
|
||||
return fmt.Errorf("retrieving instance metadata returned invalid results")
|
||||
}
|
||||
|
||||
// convert instance metadata to map to handle duplicate keys correctly
|
||||
metadataMap := extractInstanceMetadata(instance.Metadata, key, false)
|
||||
metadataMap[key] = value
|
||||
// convert instance metadata back to flat list
|
||||
metadata := flattenInstanceMetadata(metadataMap, instance.Metadata.Fingerprint, instance.Metadata.Kind)
|
||||
|
||||
if err := c.updateInstanceMetadata(ctx, project, zone, instanceName, metadata); err != nil {
|
||||
return fmt.Errorf("setting instance metadata %v: %v: %w", key, value, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnsetInstanceMetadata modifies a key value pair of metadata for the instance specified by project, zone and instanceName.
|
||||
func (c *Client) UnsetInstanceMetadata(ctx context.Context, project, zone, instanceName, key string) error {
|
||||
instance, err := c.getComputeInstance(ctx, project, zone, instanceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving instance metadata: %w", err)
|
||||
}
|
||||
if instance == nil || instance.Metadata == nil {
|
||||
return fmt.Errorf("retrieving instance metadata returned invalid results")
|
||||
}
|
||||
|
||||
// convert instance metadata to map to handle duplicate keys correctly
|
||||
// and skip the key to be removed
|
||||
metadataMap := extractInstanceMetadata(instance.Metadata, key, true)
|
||||
|
||||
// convert instance metadata back to flat list
|
||||
metadata := flattenInstanceMetadata(metadataMap, instance.Metadata.Fingerprint, instance.Metadata.Kind)
|
||||
|
||||
if err := c.updateInstanceMetadata(ctx, project, zone, instanceName, metadata); err != nil {
|
||||
return fmt.Errorf("unsetting instance metadata key %v: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetrieveSubnetworkAliasCIDR returns the alias CIDR of the subnetwork specified by project, zone and subnetworkName.
|
||||
func (c *Client) RetrieveSubnetworkAliasCIDR(ctx context.Context, project, zone, instanceName string) (string, error) {
|
||||
instance, err := c.getComputeInstance(ctx, project, zone, instanceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if instance == nil || instance.NetworkInterfaces == nil || len(instance.NetworkInterfaces) == 0 || instance.NetworkInterfaces[0].Subnetwork == nil {
|
||||
return "", fmt.Errorf("retrieving instance network interfaces failed")
|
||||
}
|
||||
subnetworkURL := *instance.NetworkInterfaces[0].Subnetwork
|
||||
subnetworkURLFragments := strings.Split(subnetworkURL, "/")
|
||||
subnetworkName := subnetworkURLFragments[len(subnetworkURLFragments)-1]
|
||||
|
||||
// convert:
|
||||
// zone --> region
|
||||
// europe-west3-b --> europe-west3
|
||||
region := zoneFromRegionRegex.FindString(zone)
|
||||
if region == "" {
|
||||
return "", fmt.Errorf("invalid zone %s", zone)
|
||||
}
|
||||
|
||||
req := &computepb.GetSubnetworkRequest{
|
||||
Project: project,
|
||||
Region: region,
|
||||
Subnetwork: subnetworkName,
|
||||
}
|
||||
subnetwork, err := c.subnetworkAPI.Get(ctx, req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("retrieving subnetwork alias CIDR failed: %w", err)
|
||||
}
|
||||
if subnetwork == nil || subnetwork.IpCidrRange == nil || *subnetwork.IpCidrRange == "" {
|
||||
return "", fmt.Errorf("retrieving subnetwork alias CIDR returned invalid results")
|
||||
}
|
||||
return *subnetwork.IpCidrRange, nil
|
||||
}
|
||||
|
||||
// RetrieveLoadBalancerIP returns the IP address of the load balancer specified by project, zone and loadBalancerName.
|
||||
func (c *Client) RetrieveLoadBalancerIP(ctx context.Context, project, zone string) (string, error) {
|
||||
uid, err := c.uid()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
region := zoneFromRegionRegex.FindString(zone)
|
||||
if region == "" {
|
||||
return "", fmt.Errorf("invalid zone %s", zone)
|
||||
}
|
||||
|
||||
req := &computepb.ListForwardingRulesRequest{
|
||||
Region: region,
|
||||
Project: project,
|
||||
}
|
||||
iter := c.forwardingRulesAPI.List(ctx, req)
|
||||
for {
|
||||
resp, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("retrieving load balancer IP failed: %w", err)
|
||||
}
|
||||
if resp.Labels["constellation-uid"] == uid {
|
||||
return *resp.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("retrieving load balancer IP failed: load balancer not found")
|
||||
}
|
||||
|
||||
// Close closes the instanceAPI client.
|
||||
func (c *Client) Close() error {
|
||||
if err := c.subnetworkAPI.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.forwardingRulesAPI.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.instanceAPI.Close()
|
||||
}
|
||||
|
||||
func (c *Client) getComputeInstance(ctx context.Context, project, zone, instanceName string) (*computepb.Instance, error) {
|
||||
instanceGetReq := &computepb.GetInstanceRequest{
|
||||
Project: project,
|
||||
Zone: zone,
|
||||
Instance: instanceName,
|
||||
}
|
||||
instance, err := c.instanceAPI.Get(ctx, instanceGetReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving compute instance: %w", err)
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// updateInstanceMetadata updates all instance metadata key-value pairs.
|
||||
func (c *Client) updateInstanceMetadata(ctx context.Context, project, zone, instanceName string, metadata *computepb.Metadata) error {
|
||||
setMetadataReq := &computepb.SetMetadataInstanceRequest{
|
||||
Project: project,
|
||||
Zone: zone,
|
||||
Instance: instanceName,
|
||||
MetadataResource: metadata,
|
||||
}
|
||||
|
||||
if _, err := c.instanceAPI.SetMetadata(ctx, setMetadataReq); err != nil {
|
||||
return fmt.Errorf("updating instance metadata: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// uid retrieves the current instances uid.
|
||||
func (c *Client) uid() (string, error) {
|
||||
// API endpoint: http://metadata.google.internal/computeMetadata/v1/instance/attributes/constellation-uid
|
||||
uid, err := c.RetrieveInstanceMetadata(constellationUIDMetadataKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("retrieving constellation uid: %w", err)
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
// extractPrivateIPs extracts private interface IPs from a list of interfaces.
|
||||
func extractPrivateIPs(interfaces []*computepb.NetworkInterface) []string {
|
||||
ips := []string{}
|
||||
for _, interf := range interfaces {
|
||||
if interf == nil || interf.NetworkIP == nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, *interf.NetworkIP)
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// extractPublicIPs extracts public interface IPs from a list of interfaces.
|
||||
func extractPublicIPs(interfaces []*computepb.NetworkInterface) []string {
|
||||
ips := []string{}
|
||||
for _, interf := range interfaces {
|
||||
if interf == nil || interf.AccessConfigs == nil {
|
||||
continue
|
||||
}
|
||||
for _, accessConfig := range interf.AccessConfigs {
|
||||
if accessConfig == nil || accessConfig.NatIP == nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, *accessConfig.NatIP)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// extractAliasIPRanges extracts alias interface IPs from a list of interfaces.
|
||||
func extractAliasIPRanges(interfaces []*computepb.NetworkInterface) []string {
|
||||
ips := []string{}
|
||||
for _, interf := range interfaces {
|
||||
if interf == nil || interf.AliasIpRanges == nil {
|
||||
continue
|
||||
}
|
||||
for _, aliasIP := range interf.AliasIpRanges {
|
||||
if aliasIP == nil || aliasIP.IpCidrRange == nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, *aliasIP.IpCidrRange)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// extractSSHKeys extracts SSH keys from GCP instance metadata.
|
||||
// reference: https://cloud.google.com/compute/docs/connect/add-ssh-keys .
|
||||
func extractSSHKeys(metadata map[string]string) map[string][]string {
|
||||
sshKeysRaw, ok := metadata[gcpSSHMetadataKey]
|
||||
if !ok {
|
||||
// ignore missing metadata entry
|
||||
return map[string][]string{}
|
||||
}
|
||||
|
||||
sshKeyLines := strings.Split(sshKeysRaw, "\n")
|
||||
keys := map[string][]string{}
|
||||
for _, sshKeyRaw := range sshKeyLines {
|
||||
keyParts := strings.SplitN(sshKeyRaw, ":", 2)
|
||||
if len(keyParts) != 2 {
|
||||
continue
|
||||
}
|
||||
username := keyParts[0]
|
||||
keyParts = strings.SplitN(keyParts[1], " ", 3)
|
||||
if len(keyParts) < 2 {
|
||||
continue
|
||||
}
|
||||
keyValue := fmt.Sprintf("%s %s", keyParts[0], keyParts[1])
|
||||
keys[username] = append(keys[username], keyValue)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// convertToCoreInstance converts a *computepb.Instance to a core.Instance.
|
||||
func convertToCoreInstance(in *computepb.Instance, project string, zone string) (metadata.InstanceMetadata, error) {
|
||||
if in.Name == nil {
|
||||
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving instance from compute API client returned invalid instance Name: %v", in.Name)
|
||||
}
|
||||
mdata := extractInstanceMetadata(in.Metadata, "", false)
|
||||
return metadata.InstanceMetadata{
|
||||
Name: *in.Name,
|
||||
ProviderID: gcpshared.JoinProviderID(project, zone, *in.Name),
|
||||
Role: extractRole(mdata),
|
||||
PrivateIPs: extractPrivateIPs(in.NetworkInterfaces),
|
||||
PublicIPs: extractPublicIPs(in.NetworkInterfaces),
|
||||
AliasIPRanges: extractAliasIPRanges(in.NetworkInterfaces),
|
||||
SSHKeys: extractSSHKeys(mdata),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractInstanceMetadata will extract the list of instance metadata key-value pairs into a map.
|
||||
// If "skipKey" is true, "key" will be skipped.
|
||||
func extractInstanceMetadata(in *computepb.Metadata, key string, skipKey bool) map[string]string {
|
||||
metadataMap := map[string]string{}
|
||||
for _, item := range in.Items {
|
||||
if item == nil || item.Key == nil || item.Value == nil {
|
||||
continue
|
||||
}
|
||||
if skipKey && *item.Key == key {
|
||||
continue
|
||||
}
|
||||
metadataMap[*item.Key] = *item.Value
|
||||
}
|
||||
return metadataMap
|
||||
}
|
||||
|
||||
// flattenInstanceMetadata takes a map of metadata key-value pairs and returns a flat list of computepb.Items inside computepb.Metadata.
|
||||
func flattenInstanceMetadata(metadataMap map[string]string, fingerprint, kind *string) *computepb.Metadata {
|
||||
metadata := &computepb.Metadata{
|
||||
Fingerprint: fingerprint,
|
||||
Kind: kind,
|
||||
Items: make([]*computepb.Items, len(metadataMap)),
|
||||
}
|
||||
i := 0
|
||||
for mapKey, mapValue := range metadataMap {
|
||||
metadata.Items[i] = &computepb.Items{Key: proto.String(mapKey), Value: proto.String(mapValue)}
|
||||
i++
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
1044
bootstrapper/cloudprovider/gcp/client_test.go
Normal file
1044
bootstrapper/cloudprovider/gcp/client_test.go
Normal file
File diff suppressed because it is too large
Load diff
27
bootstrapper/cloudprovider/gcp/cloudnodemanager.go
Normal file
27
bootstrapper/cloudprovider/gcp/cloudnodemanager.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package gcp
|
||||
|
||||
// CloudNodeManager holds the GCP cloud-node-manager configuration.
|
||||
type CloudNodeManager struct{}
|
||||
|
||||
// Image returns the container image used to provide cloud-node-manager for the cloud-provider.
|
||||
// Not used on GCP.
|
||||
func (c *CloudNodeManager) Image() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Path returns the path used by cloud-node-manager executable within the container image.
|
||||
// Not used on GCP.
|
||||
func (c *CloudNodeManager) Path() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-node-manager command.
|
||||
// Not used on GCP.
|
||||
func (c *CloudNodeManager) ExtraArgs() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Supported is used to determine if cloud node manager is implemented for this cloud provider.
|
||||
func (c *CloudNodeManager) Supported() bool {
|
||||
return false
|
||||
}
|
||||
17
bootstrapper/cloudprovider/gcp/cloudnodemanager_test.go
Normal file
17
bootstrapper/cloudprovider/gcp/cloudnodemanager_test.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrivialCNMFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cloud := CloudNodeManager{}
|
||||
|
||||
assert.Empty(cloud.Image())
|
||||
assert.Empty(cloud.Path())
|
||||
assert.Empty(cloud.ExtraArgs())
|
||||
assert.False(cloud.Supported())
|
||||
}
|
||||
45
bootstrapper/cloudprovider/gcp/logger.go
Normal file
45
bootstrapper/cloudprovider/gcp/logger.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"cloud.google.com/go/logging"
|
||||
"github.com/edgelesssys/constellation/internal/gcpshared"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
client *logging.Client
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewLogger creates a new Cloud Logger for GCP.
|
||||
// https://cloud.google.com/logging/docs/setup/go
|
||||
func NewLogger(ctx context.Context, providerID string, logName string) (*Logger, error) {
|
||||
projectID, _, _, err := gcpshared.SplitProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := logging.NewClient(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := client.Logger(logName).StandardLogger(logging.Info)
|
||||
|
||||
return &Logger{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Disclose stores log information in GCP Cloud Logging! Do **NOT** log sensitive
|
||||
// information!
|
||||
func (l *Logger) Disclose(msg string) {
|
||||
l.logger.Println(msg)
|
||||
}
|
||||
|
||||
// Close waits for all buffer to be written.
|
||||
func (l *Logger) Close() error {
|
||||
return l.client.Close()
|
||||
}
|
||||
128
bootstrapper/cloudprovider/gcp/metadata.go
Normal file
128
bootstrapper/cloudprovider/gcp/metadata.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/internal/gcpshared"
|
||||
)
|
||||
|
||||
// API handles all GCP API requests.
|
||||
type API interface {
|
||||
// RetrieveInstances retrieves a list of all accessible GCP instances with their metadata.
|
||||
RetrieveInstances(ctx context.Context, project, zone string) ([]metadata.InstanceMetadata, error)
|
||||
// RetrieveInstances retrieves a single GCP instances with its metadata.
|
||||
RetrieveInstance(ctx context.Context, project, zone, instanceName string) (metadata.InstanceMetadata, error)
|
||||
// RetrieveInstanceMetadata retrieves the GCP instance metadata of the current instance.
|
||||
RetrieveInstanceMetadata(attr string) (string, error)
|
||||
// RetrieveProjectID retrieves the GCP projectID containing the current instance.
|
||||
RetrieveProjectID() (string, error)
|
||||
// RetrieveZone retrieves the GCP zone containing the current instance.
|
||||
RetrieveZone() (string, error)
|
||||
// RetrieveInstanceName retrieves the instance name of the current instance.
|
||||
RetrieveInstanceName() (string, error)
|
||||
// RetrieveSubnetworkAliasCIDR retrieves the subnetwork CIDR of the current instance.
|
||||
RetrieveSubnetworkAliasCIDR(ctx context.Context, project, zone, instanceName string) (string, error)
|
||||
// RetrieveLoadBalancerIP retrieves the load balancer IP of the current instance.
|
||||
RetrieveLoadBalancerIP(ctx context.Context, project, zone string) (string, error)
|
||||
// SetInstanceMetadata sets metadata key: value of the instance specified by project, zone and instanceName.
|
||||
SetInstanceMetadata(ctx context.Context, project, zone, instanceName, key, value string) error
|
||||
// UnsetInstanceMetadata removes a metadata key-value pair of the instance specified by project, zone and instanceName.
|
||||
UnsetInstanceMetadata(ctx context.Context, project, zone, instanceName, key string) error
|
||||
}
|
||||
|
||||
// Metadata implements core.ProviderMetadata interface.
|
||||
type Metadata struct {
|
||||
api API
|
||||
}
|
||||
|
||||
// New creates a new Provider with real API and FS.
|
||||
func New(api API) *Metadata {
|
||||
return &Metadata{
|
||||
api: api,
|
||||
}
|
||||
}
|
||||
|
||||
// List retrieves all instances belonging to the current constellation.
|
||||
func (m *Metadata) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
project, err := m.api.RetrieveProjectID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zone, err := m.api.RetrieveZone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instances, err := m.api.RetrieveInstances(ctx, project, zone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving instances list from GCP api: %w", err)
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// Self retrieves the current instance.
|
||||
func (m *Metadata) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||
project, err := m.api.RetrieveProjectID()
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
zone, err := m.api.RetrieveZone()
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
instanceName, err := m.api.RetrieveInstanceName()
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
return m.api.RetrieveInstance(ctx, project, zone, instanceName)
|
||||
}
|
||||
|
||||
// GetInstance retrieves an instance using its providerID.
|
||||
func (m *Metadata) GetInstance(ctx context.Context, providerID string) (metadata.InstanceMetadata, error) {
|
||||
project, zone, instanceName, err := gcpshared.SplitProviderID(providerID)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, fmt.Errorf("invalid providerID: %w", err)
|
||||
}
|
||||
return m.api.RetrieveInstance(ctx, project, zone, instanceName)
|
||||
}
|
||||
|
||||
// GetSubnetworkCIDR returns the subnetwork CIDR of the current instance.
|
||||
func (m *Metadata) GetSubnetworkCIDR(ctx context.Context) (string, error) {
|
||||
project, err := m.api.RetrieveProjectID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
zone, err := m.api.RetrieveZone()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
instanceName, err := m.api.RetrieveInstanceName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return m.api.RetrieveSubnetworkAliasCIDR(ctx, project, zone, instanceName)
|
||||
}
|
||||
|
||||
// SupportsLoadBalancer returns true if the cloud provider supports load balancers.
|
||||
func (m *Metadata) SupportsLoadBalancer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetLoadBalancerIP returns the IP of the load balancer.
|
||||
func (m *Metadata) GetLoadBalancerIP(ctx context.Context) (string, error) {
|
||||
project, err := m.api.RetrieveProjectID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
zone, err := m.api.RetrieveZone()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return m.api.RetrieveLoadBalancerIP(ctx, project, zone)
|
||||
}
|
||||
|
||||
// Supported is used to determine if metadata API is implemented for this cloud provider.
|
||||
func (m *Metadata) Supported() bool {
|
||||
return true
|
||||
}
|
||||
313
bootstrapper/cloudprovider/gcp/metadata_test.go
Normal file
313
bootstrapper/cloudprovider/gcp/metadata_test.go
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
err := errors.New("some err")
|
||||
uid := "1234"
|
||||
instancesGenerator := func() *[]metadata.InstanceMetadata {
|
||||
return &[]metadata.InstanceMetadata{
|
||||
{
|
||||
Name: "someInstance",
|
||||
ProviderID: "gce://someProject/someZone/someInstance",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
client stubGCPClient
|
||||
instancesGenerator func() *[]metadata.InstanceMetadata
|
||||
instancesMutator func(*[]metadata.InstanceMetadata)
|
||||
wantErr bool
|
||||
wantInstances []metadata.InstanceMetadata
|
||||
}{
|
||||
"retrieve works": {
|
||||
client: stubGCPClient{
|
||||
projectID: "someProjectID",
|
||||
zone: "someZone",
|
||||
retrieveInstanceMetadaValues: map[string]string{
|
||||
"constellation-uid": uid,
|
||||
},
|
||||
},
|
||||
instancesGenerator: instancesGenerator,
|
||||
wantInstances: []metadata.InstanceMetadata{
|
||||
{
|
||||
Name: "someInstance",
|
||||
ProviderID: "gce://someProject/someZone/someInstance",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"retrieve error is detected": {
|
||||
client: stubGCPClient{
|
||||
projectID: "someProjectID",
|
||||
zone: "someZone",
|
||||
retrieveInstanceMetadaValues: map[string]string{
|
||||
"constellation-uid": uid,
|
||||
},
|
||||
retrieveInstancesErr: err,
|
||||
},
|
||||
instancesGenerator: instancesGenerator,
|
||||
wantErr: true,
|
||||
},
|
||||
"project metadata retrieval error is detected": {
|
||||
client: stubGCPClient{
|
||||
retrieveProjectIDErr: err,
|
||||
},
|
||||
instancesGenerator: instancesGenerator,
|
||||
wantErr: true,
|
||||
},
|
||||
"zone retrieval error is detected": {
|
||||
client: stubGCPClient{
|
||||
retrieveZoneErr: err,
|
||||
},
|
||||
instancesGenerator: instancesGenerator,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
tc.client.retrieveInstancesValues = *tc.instancesGenerator()
|
||||
if tc.instancesMutator != nil {
|
||||
tc.instancesMutator(&tc.client.retrieveInstancesValues)
|
||||
}
|
||||
metadata := New(&tc.client)
|
||||
instances, err := metadata.List(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.ElementsMatch(tc.wantInstances, instances)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelf(t *testing.T) {
|
||||
err := errors.New("some err")
|
||||
uid := "1234"
|
||||
|
||||
testCases := map[string]struct {
|
||||
client stubGCPClient
|
||||
wantErr bool
|
||||
wantInstance metadata.InstanceMetadata
|
||||
}{
|
||||
"retrieve works": {
|
||||
client: stubGCPClient{
|
||||
projectID: "someProjectID",
|
||||
zone: "someZone",
|
||||
retrieveInstanceValue: metadata.InstanceMetadata{
|
||||
Name: "someInstance",
|
||||
ProviderID: "gce://someProject/someZone/someInstance",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
},
|
||||
wantInstance: metadata.InstanceMetadata{
|
||||
Name: "someInstance",
|
||||
ProviderID: "gce://someProject/someZone/someInstance",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
},
|
||||
"retrieve error is detected": {
|
||||
client: stubGCPClient{
|
||||
projectID: "someProjectID",
|
||||
zone: "someZone",
|
||||
retrieveInstanceMetadaValues: map[string]string{
|
||||
"constellation-uid": uid,
|
||||
},
|
||||
retrieveInstanceErr: err,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"project id retrieval error is detected": {
|
||||
client: stubGCPClient{
|
||||
retrieveProjectIDErr: err,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"zone retrieval error is detected": {
|
||||
client: stubGCPClient{
|
||||
retrieveZoneErr: err,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"instance name retrieval error is detected": {
|
||||
client: stubGCPClient{
|
||||
retrieveInstanceNameErr: err,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
cloud := New(&tc.client)
|
||||
instance, err := cloud.Self(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantInstance, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInstance(t *testing.T) {
|
||||
err := errors.New("some err")
|
||||
|
||||
testCases := map[string]struct {
|
||||
providerID string
|
||||
client stubGCPClient
|
||||
wantErr bool
|
||||
wantInstance metadata.InstanceMetadata
|
||||
}{
|
||||
"retrieve works": {
|
||||
providerID: "gce://someProject/someZone/someInstance",
|
||||
client: stubGCPClient{
|
||||
retrieveInstanceValue: metadata.InstanceMetadata{
|
||||
Name: "someInstance",
|
||||
ProviderID: "gce://someProject/someZone/someInstance",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
},
|
||||
wantInstance: metadata.InstanceMetadata{
|
||||
Name: "someInstance",
|
||||
ProviderID: "gce://someProject/someZone/someInstance",
|
||||
PrivateIPs: []string{"192.0.2.0"},
|
||||
},
|
||||
},
|
||||
"retrieve error is detected": {
|
||||
providerID: "gce://someProject/someZone/someInstance",
|
||||
client: stubGCPClient{
|
||||
retrieveInstanceErr: err,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"malformed providerID with too many fields is detected": {
|
||||
providerID: "gce://someProject/someZone/someInstance/tooMany/fields",
|
||||
wantErr: true,
|
||||
},
|
||||
"malformed providerID with too few fields is detected": {
|
||||
providerID: "gce://someProject",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
cloud := New(&tc.client)
|
||||
instance, err := cloud.GetInstance(context.Background(), tc.providerID)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantInstance, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubGCPClient struct {
|
||||
retrieveInstanceValue metadata.InstanceMetadata
|
||||
retrieveInstanceErr error
|
||||
retrieveInstancesValues []metadata.InstanceMetadata
|
||||
retrieveInstancesErr error
|
||||
retrieveInstanceMetadaValues map[string]string
|
||||
retrieveInstanceMetadataErr error
|
||||
retrieveSubentworkAliasErr error
|
||||
projectID string
|
||||
zone string
|
||||
instanceName string
|
||||
loadBalancerIP string
|
||||
retrieveProjectIDErr error
|
||||
retrieveZoneErr error
|
||||
retrieveInstanceNameErr error
|
||||
setInstanceMetadataErr error
|
||||
unsetInstanceMetadataErr error
|
||||
retrieveLoadBalancerErr error
|
||||
|
||||
instanceMetadataProjects []string
|
||||
instanceMetadataZones []string
|
||||
instanceMetadataInstanceNames []string
|
||||
instanceMetadataKeys []string
|
||||
instanceMetadataValues []string
|
||||
|
||||
unsetMetadataProjects []string
|
||||
unsetMetadataZones []string
|
||||
unsetMetadataInstanceNames []string
|
||||
unsetMetadataKeys []string
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveInstances(ctx context.Context, project, zone string) ([]metadata.InstanceMetadata, error) {
|
||||
return s.retrieveInstancesValues, s.retrieveInstancesErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveInstance(ctx context.Context, project, zone string, instanceName string) (metadata.InstanceMetadata, error) {
|
||||
return s.retrieveInstanceValue, s.retrieveInstanceErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveInstanceMetadata(attr string) (string, error) {
|
||||
return s.retrieveInstanceMetadaValues[attr], s.retrieveInstanceMetadataErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveProjectID() (string, error) {
|
||||
return s.projectID, s.retrieveProjectIDErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveZone() (string, error) {
|
||||
return s.zone, s.retrieveZoneErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveInstanceName() (string, error) {
|
||||
return s.instanceName, s.retrieveInstanceNameErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveLoadBalancerIP(ctx context.Context, project, zone string) (string, error) {
|
||||
return s.loadBalancerIP, s.retrieveLoadBalancerErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) SetInstanceMetadata(ctx context.Context, project, zone, instanceName, key, value string) error {
|
||||
s.instanceMetadataProjects = append(s.instanceMetadataProjects, project)
|
||||
s.instanceMetadataZones = append(s.instanceMetadataZones, zone)
|
||||
s.instanceMetadataInstanceNames = append(s.instanceMetadataInstanceNames, instanceName)
|
||||
s.instanceMetadataKeys = append(s.instanceMetadataKeys, key)
|
||||
s.instanceMetadataValues = append(s.instanceMetadataValues, value)
|
||||
|
||||
return s.setInstanceMetadataErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) UnsetInstanceMetadata(ctx context.Context, project, zone, instanceName, key string) error {
|
||||
s.unsetMetadataProjects = append(s.unsetMetadataProjects, project)
|
||||
s.unsetMetadataZones = append(s.unsetMetadataZones, zone)
|
||||
s.unsetMetadataInstanceNames = append(s.unsetMetadataInstanceNames, instanceName)
|
||||
s.unsetMetadataKeys = append(s.unsetMetadataKeys, key)
|
||||
|
||||
return s.unsetInstanceMetadataErr
|
||||
}
|
||||
|
||||
func (s *stubGCPClient) RetrieveSubnetworkAliasCIDR(ctx context.Context, project, zone, instanceName string) (string, error) {
|
||||
return "", s.retrieveSubentworkAliasErr
|
||||
}
|
||||
19
bootstrapper/cloudprovider/gcp/role.go
Normal file
19
bootstrapper/cloudprovider/gcp/role.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
)
|
||||
|
||||
const roleMetadataKey = "constellation-role"
|
||||
|
||||
// extractRole extracts role from cloud provider metadata.
|
||||
func extractRole(metadata map[string]string) role.Role {
|
||||
switch metadata[roleMetadataKey] {
|
||||
case role.ControlPlane.String():
|
||||
return role.ControlPlane
|
||||
case role.Worker.String():
|
||||
return role.Worker
|
||||
default:
|
||||
return role.Unknown
|
||||
}
|
||||
}
|
||||
55
bootstrapper/cloudprovider/gcp/role_test.go
Normal file
55
bootstrapper/cloudprovider/gcp/role_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m,
|
||||
// https://github.com/census-instrumentation/opencensus-go/issues/1262
|
||||
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestExtractRole(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
metadata map[string]string
|
||||
wantRole role.Role
|
||||
}{
|
||||
"bootstrapper role": {
|
||||
metadata: map[string]string{
|
||||
roleMetadataKey: role.ControlPlane.String(),
|
||||
},
|
||||
wantRole: role.ControlPlane,
|
||||
},
|
||||
"node role": {
|
||||
metadata: map[string]string{
|
||||
roleMetadataKey: role.Worker.String(),
|
||||
},
|
||||
wantRole: role.Worker,
|
||||
},
|
||||
"unknown role": {
|
||||
metadata: map[string]string{
|
||||
roleMetadataKey: "some-unknown-role",
|
||||
},
|
||||
wantRole: role.Unknown,
|
||||
},
|
||||
"no role": {
|
||||
wantRole: role.Unknown,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
role := extractRole(tc.metadata)
|
||||
|
||||
assert.Equal(tc.wantRole, role)
|
||||
})
|
||||
}
|
||||
}
|
||||
80
bootstrapper/cloudprovider/gcp/wrappers.go
Normal file
80
bootstrapper/cloudprovider/gcp/wrappers.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
)
|
||||
|
||||
type instanceClient struct {
|
||||
*compute.InstancesClient
|
||||
}
|
||||
|
||||
func (c *instanceClient) Close() error {
|
||||
return c.InstancesClient.Close()
|
||||
}
|
||||
|
||||
func (c *instanceClient) List(ctx context.Context, req *computepb.ListInstancesRequest,
|
||||
opts ...gax.CallOption,
|
||||
) InstanceIterator {
|
||||
return c.InstancesClient.List(ctx, req)
|
||||
}
|
||||
|
||||
type subnetworkClient struct {
|
||||
*compute.SubnetworksClient
|
||||
}
|
||||
|
||||
func (c *subnetworkClient) Close() error {
|
||||
return c.SubnetworksClient.Close()
|
||||
}
|
||||
|
||||
func (c *subnetworkClient) List(ctx context.Context, req *computepb.ListSubnetworksRequest,
|
||||
opts ...gax.CallOption,
|
||||
) SubnetworkIterator {
|
||||
return c.SubnetworksClient.List(ctx, req)
|
||||
}
|
||||
|
||||
func (c *subnetworkClient) Get(ctx context.Context, req *computepb.GetSubnetworkRequest,
|
||||
opts ...gax.CallOption,
|
||||
) (*computepb.Subnetwork, error) {
|
||||
return c.SubnetworksClient.Get(ctx, req)
|
||||
}
|
||||
|
||||
type forwardingRulesClient struct {
|
||||
*compute.ForwardingRulesClient
|
||||
}
|
||||
|
||||
func (c *forwardingRulesClient) Close() error {
|
||||
return c.ForwardingRulesClient.Close()
|
||||
}
|
||||
|
||||
func (c *forwardingRulesClient) List(ctx context.Context, req *computepb.ListForwardingRulesRequest,
|
||||
opts ...gax.CallOption,
|
||||
) ForwardingRuleIterator {
|
||||
return c.ForwardingRulesClient.List(ctx, req)
|
||||
}
|
||||
|
||||
type metadataClient struct{}
|
||||
|
||||
func (c *metadataClient) InstanceAttributeValue(attr string) (string, error) {
|
||||
return metadata.InstanceAttributeValue(attr)
|
||||
}
|
||||
|
||||
func (c *metadataClient) ProjectID() (string, error) {
|
||||
return metadata.ProjectID()
|
||||
}
|
||||
|
||||
func (c *metadataClient) Zone() (string, error) {
|
||||
return metadata.Zone()
|
||||
}
|
||||
|
||||
func (c *metadataClient) InstanceName() (string, error) {
|
||||
return metadata.InstanceName()
|
||||
}
|
||||
|
||||
func (c *metadataClient) ProjectAttributeValue(attr string) (string, error) {
|
||||
return metadata.ProjectAttributeValue(attr)
|
||||
}
|
||||
20
bootstrapper/cloudprovider/gcp/writer.go
Normal file
20
bootstrapper/cloudprovider/gcp/writer.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Writer implements ConfigWriter.
|
||||
type Writer struct {
|
||||
fs afero.Afero
|
||||
}
|
||||
|
||||
// WriteGCEConf persists the GCE config on disk.
|
||||
func (w *Writer) WriteGCEConf(config string) error {
|
||||
if err := w.fs.WriteFile("/etc/gce.conf", []byte(config), 0o644); err != nil {
|
||||
return fmt.Errorf("writing gce config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
54
bootstrapper/cloudprovider/gcp/writer_test.go
Normal file
54
bootstrapper/cloudprovider/gcp/writer_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteGCEConf(t *testing.T) {
|
||||
config := "someConfig"
|
||||
|
||||
testCases := map[string]struct {
|
||||
fs afero.Afero
|
||||
wantValue string
|
||||
wantErr bool
|
||||
}{
|
||||
"write works": {
|
||||
fs: afero.Afero{
|
||||
Fs: afero.NewMemMapFs(),
|
||||
},
|
||||
wantValue: config,
|
||||
wantErr: false,
|
||||
},
|
||||
"write fails": {
|
||||
fs: afero.Afero{
|
||||
Fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
writer := Writer{
|
||||
fs: tc.fs,
|
||||
}
|
||||
err := writer.WriteGCEConf(config)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
value, err := tc.fs.ReadFile("/etc/gce.conf")
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantValue, string(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
13
bootstrapper/cloudprovider/images.go
Normal file
13
bootstrapper/cloudprovider/images.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package cloudprovider
|
||||
|
||||
const (
|
||||
// CloudControllerManagerImageAWS is the CCM image used on AWS.
|
||||
CloudControllerManagerImageAWS = "us.gcr.io/k8s-artifacts-prod/provider-aws/cloud-controller-manager:v1.22.0-alpha.0"
|
||||
// CloudControllerManagerImageGCP is the CCM image used on GCP.
|
||||
// TODO: use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available.
|
||||
CloudControllerManagerImageGCP = "ghcr.io/edgelesssys/cloud-provider-gcp:sha-2f6a5b07fc2d37f24f8ff725132f87584d627d8f"
|
||||
// CloudControllerManagerImageAzure is the CCM image used on Azure.
|
||||
CloudControllerManagerImageAzure = "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.23.11"
|
||||
// CloudNodeManagerImageAzure is the cloud-node-manager image used on Azure.
|
||||
CloudNodeManagerImageAzure = "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.23.11"
|
||||
)
|
||||
39
bootstrapper/cloudprovider/qemu/autoscaler.go
Normal file
39
bootstrapper/cloudprovider/qemu/autoscaler.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Autoscaler holds the QEMU cluster-autoscaler configuration.
|
||||
type Autoscaler struct{}
|
||||
|
||||
// Name returns the cloud-provider name as used by k8s cluster-autoscaler.
|
||||
func (a Autoscaler) Name() string {
|
||||
return "qemu"
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cluster-autoscaler.
|
||||
func (a Autoscaler) Secrets(providerID, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
return resources.Secrets{}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cluster-autoscaler.
|
||||
func (a Autoscaler) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{}
|
||||
}
|
||||
|
||||
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cluster-autoscaler.
|
||||
func (a Autoscaler) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cluster-autoscaler.
|
||||
func (a Autoscaler) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{}
|
||||
}
|
||||
|
||||
// Supported is used to determine if we support autoscaling for the cloud provider.
|
||||
func (a Autoscaler) Supported() bool {
|
||||
return false
|
||||
}
|
||||
72
bootstrapper/cloudprovider/qemu/ccm.go
Normal file
72
bootstrapper/cloudprovider/qemu/ccm.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// CloudControllerManager holds the QEMU cloud-controller-manager configuration.
|
||||
type CloudControllerManager struct{}
|
||||
|
||||
// Image returns the container image used to provide cloud-controller-manager for the cloud-provider.
|
||||
func (c CloudControllerManager) Image() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Path returns the path used by cloud-controller-manager executable within the container image.
|
||||
func (c CloudControllerManager) Path() string {
|
||||
return "/qemu-cloud-controller-manager"
|
||||
}
|
||||
|
||||
// Name returns the cloud-provider name as used by k8s cloud-controller-manager (k8s.gcr.io/cloud-controller-manager).
|
||||
func (c CloudControllerManager) Name() string {
|
||||
return "qemu"
|
||||
}
|
||||
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-controller-manager command.
|
||||
func (c CloudControllerManager) ExtraArgs() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// ConfigMaps returns a list of ConfigMaps to deploy together with the k8s cloud-controller-manager
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/configmap/ .
|
||||
func (c CloudControllerManager) ConfigMaps(instance metadata.InstanceMetadata) (resources.ConfigMaps, error) {
|
||||
return resources.ConfigMaps{}, nil
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/secret/ .
|
||||
func (c CloudControllerManager) Secrets(ctx context.Context, providerID, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
return resources.Secrets{}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/storage/volumes/ .
|
||||
func (c CloudControllerManager) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{}
|
||||
}
|
||||
|
||||
// VolumeMounts a list of of volume mounts to deploy together with the k8s cloud-controller-manager.
|
||||
func (c CloudControllerManager) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cloud-controller-manager.
|
||||
func (c CloudControllerManager) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{}
|
||||
}
|
||||
|
||||
// PrepareInstance is called on every instance before deploying the cloud-controller-manager.
|
||||
// Allows for cloud-provider specific hooks.
|
||||
func (c CloudControllerManager) PrepareInstance(instance metadata.InstanceMetadata, vpnIP string) error {
|
||||
// no specific hook required.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Supported is used to determine if cloud controller manager is implemented for this cloud provider.
|
||||
func (c CloudControllerManager) Supported() bool {
|
||||
return false
|
||||
}
|
||||
27
bootstrapper/cloudprovider/qemu/cloudnodemanager.go
Normal file
27
bootstrapper/cloudprovider/qemu/cloudnodemanager.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package qemu
|
||||
|
||||
// CloudNodeManager holds the QEMU cloud-node-manager configuration.
|
||||
type CloudNodeManager struct{}
|
||||
|
||||
// Image returns the container image used to provide cloud-node-manager for the cloud-provider.
|
||||
// Not used on QEMU.
|
||||
func (c *CloudNodeManager) Image() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Path returns the path used by cloud-node-manager executable within the container image.
|
||||
// Not used on QEMU.
|
||||
func (c *CloudNodeManager) Path() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-node-manager command.
|
||||
// Not used on QEMU.
|
||||
func (c *CloudNodeManager) ExtraArgs() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Supported is used to determine if cloud node manager is implemented for this cloud provider.
|
||||
func (c *CloudNodeManager) Supported() bool {
|
||||
return false
|
||||
}
|
||||
96
bootstrapper/cloudprovider/qemu/metadata.go
Normal file
96
bootstrapper/cloudprovider/qemu/metadata.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
)
|
||||
|
||||
const qemuMetadataEndpoint = "10.42.0.1:8080"
|
||||
|
||||
// Metadata implements core.ProviderMetadata interface for QEMU.
|
||||
type Metadata struct{}
|
||||
|
||||
// Supported is used to determine if metadata API is implemented for this cloud provider.
|
||||
func (m *Metadata) Supported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// List retrieves all instances belonging to the current constellation.
|
||||
func (m *Metadata) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
instancesRaw, err := m.retrieveMetadata(ctx, "/peers")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var instances []metadata.InstanceMetadata
|
||||
err = json.Unmarshal(instancesRaw, &instances)
|
||||
return instances, err
|
||||
}
|
||||
|
||||
// Self retrieves the current instance.
|
||||
func (m *Metadata) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||
instanceRaw, err := m.retrieveMetadata(ctx, "/self")
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
|
||||
var instance metadata.InstanceMetadata
|
||||
err = json.Unmarshal(instanceRaw, &instance)
|
||||
return instance, err
|
||||
}
|
||||
|
||||
// GetInstance retrieves an instance using its providerID.
|
||||
func (m Metadata) GetInstance(ctx context.Context, providerID string) (metadata.InstanceMetadata, error) {
|
||||
instances, err := m.List(ctx)
|
||||
if err != nil {
|
||||
return metadata.InstanceMetadata{}, err
|
||||
}
|
||||
|
||||
for _, instance := range instances {
|
||||
if instance.ProviderID == providerID {
|
||||
return instance, nil
|
||||
}
|
||||
}
|
||||
return metadata.InstanceMetadata{}, errors.New("instance not found")
|
||||
}
|
||||
|
||||
// SupportsLoadBalancer returns true if the cloud provider supports load balancers.
|
||||
func (m Metadata) SupportsLoadBalancer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLoadBalancerIP returns the IP of the load balancer.
|
||||
func (m Metadata) GetLoadBalancerIP(ctx context.Context) (string, error) {
|
||||
panic("function *Metadata.GetLoadBalancerIP not implemented")
|
||||
}
|
||||
|
||||
// GetSubnetworkCIDR retrieves the subnetwork CIDR from cloud provider metadata.
|
||||
func (m Metadata) GetSubnetworkCIDR(ctx context.Context) (string, error) {
|
||||
return "10.244.0.0/16", nil
|
||||
}
|
||||
|
||||
func (m Metadata) retrieveMetadata(ctx context.Context, uri string) ([]byte, error) {
|
||||
url := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: qemuMetadataEndpoint,
|
||||
Path: uri,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := (&http.Client{}).Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
180
bootstrapper/cmd/bootstrapper/main.go
Normal file
180
bootstrapper/cmd/bootstrapper/main.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
azurecloud "github.com/edgelesssys/constellation/bootstrapper/cloudprovider/azure"
|
||||
gcpcloud "github.com/edgelesssys/constellation/bootstrapper/cloudprovider/gcp"
|
||||
qemucloud "github.com/edgelesssys/constellation/bootstrapper/cloudprovider/qemu"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/joinclient"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/kubectl"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/logging"
|
||||
"github.com/edgelesssys/constellation/internal/atls"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/azure"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/gcp"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/qemu"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/simulator"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/edgelesssys/constellation/internal/oid"
|
||||
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
|
||||
"github.com/spf13/afero"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultIP = "0.0.0.0"
|
||||
defaultPort = "9000"
|
||||
// ConstellationCSP is the Cloud Service Provider Constellation is running on.
|
||||
constellationCSP = "CONSTEL_CSP"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var bindIP, bindPort string
|
||||
var clusterInitJoiner clusterInitJoiner
|
||||
var metadataAPI joinclient.MetadataAPI
|
||||
var cloudLogger logging.CloudLogger
|
||||
cfg := zap.NewDevelopmentConfig()
|
||||
|
||||
logLevelUser := flag.Bool("debug", false, "enables gRPC debug output")
|
||||
flag.Parse()
|
||||
cfg.Level.SetLevel(zap.DebugLevel)
|
||||
|
||||
zapLogger, err := cfg.Build()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *logLevelUser {
|
||||
grpc_zap.ReplaceGrpcLoggerV2(zapLogger.Named("gRPC"))
|
||||
} else {
|
||||
grpc_zap.ReplaceGrpcLoggerV2(zapLogger.WithOptions(zap.IncreaseLevel(zap.WarnLevel)).Named("gRPC"))
|
||||
}
|
||||
zapLoggerCore := zapLogger.Named("core")
|
||||
|
||||
var issuer atls.Issuer
|
||||
var openTPM vtpm.TPMOpenFunc
|
||||
var fs afero.Fs
|
||||
|
||||
switch strings.ToLower(os.Getenv(constellationCSP)) {
|
||||
case "aws":
|
||||
panic("AWS cloud provider currently unsupported")
|
||||
case "gcp":
|
||||
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.GCPPCRSelection)
|
||||
if err != nil {
|
||||
// TODO: Is there a reason we use log. instead of zapLogger?
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
issuer = gcp.NewIssuer()
|
||||
|
||||
gcpClient, err := gcpcloud.NewClient(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create GCP client: %v\n", err)
|
||||
}
|
||||
metadata := gcpcloud.New(gcpClient)
|
||||
descr, err := metadata.Self(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cloudLogger, err = gcpcloud.NewLogger(ctx, descr.ProviderID, "constellation-boot-log")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
metadataAPI = metadata
|
||||
pcrsJSON, err := json.Marshal(pcrs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
clusterInitJoiner = kubernetes.New(
|
||||
"gcp", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), &gcpcloud.CloudControllerManager{},
|
||||
&gcpcloud.CloudNodeManager{}, &gcpcloud.Autoscaler{}, metadata, pcrsJSON,
|
||||
)
|
||||
bindIP = defaultIP
|
||||
bindPort = defaultPort
|
||||
openTPM = vtpm.OpenVTPM
|
||||
fs = afero.NewOsFs()
|
||||
case "azure":
|
||||
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.AzurePCRSelection)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
issuer = azure.NewIssuer()
|
||||
|
||||
metadata, err := azurecloud.NewMetadata(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cloudLogger, err = azurecloud.NewLogger(ctx, metadata)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
metadataAPI = metadata
|
||||
pcrsJSON, err := json.Marshal(pcrs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
clusterInitJoiner = kubernetes.New(
|
||||
"azure", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), azurecloud.NewCloudControllerManager(metadata),
|
||||
&azurecloud.CloudNodeManager{}, &azurecloud.Autoscaler{}, metadata, pcrsJSON,
|
||||
)
|
||||
|
||||
bindIP = defaultIP
|
||||
bindPort = defaultPort
|
||||
openTPM = vtpm.OpenVTPM
|
||||
fs = afero.NewOsFs()
|
||||
case "qemu":
|
||||
pcrs, err := vtpm.GetSelectedPCRs(vtpm.OpenVTPM, vtpm.QEMUPCRSelection)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
issuer = qemu.NewIssuer()
|
||||
|
||||
cloudLogger = qemucloud.NewLogger()
|
||||
metadata := &qemucloud.Metadata{}
|
||||
pcrsJSON, err := json.Marshal(pcrs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
clusterInitJoiner = kubernetes.New(
|
||||
"qemu", k8sapi.NewKubernetesUtil(), &k8sapi.CoreOSConfiguration{}, kubectl.New(), &qemucloud.CloudControllerManager{},
|
||||
&qemucloud.CloudNodeManager{}, &qemucloud.Autoscaler{}, metadata, pcrsJSON,
|
||||
)
|
||||
metadataAPI = metadata
|
||||
|
||||
bindIP = defaultIP
|
||||
bindPort = defaultPort
|
||||
openTPM = vtpm.OpenVTPM
|
||||
fs = afero.NewOsFs()
|
||||
default:
|
||||
issuer = atls.NewFakeIssuer(oid.Dummy{})
|
||||
clusterInitJoiner = &clusterFake{}
|
||||
metadataAPI = &providerMetadataFake{}
|
||||
cloudLogger = &logging.NopLogger{}
|
||||
bindIP = defaultIP
|
||||
bindPort = defaultPort
|
||||
var simulatedTPMCloser io.Closer
|
||||
openTPM, simulatedTPMCloser = simulator.NewSimulatedTPMOpenFunc()
|
||||
defer simulatedTPMCloser.Close()
|
||||
fs = afero.NewMemMapFs()
|
||||
}
|
||||
|
||||
fileHandler := file.NewHandler(fs)
|
||||
|
||||
run(issuer, openTPM, fileHandler, clusterInitJoiner,
|
||||
metadataAPI, bindIP,
|
||||
bindPort, zapLoggerCore, cloudLogger, fs)
|
||||
}
|
||||
67
bootstrapper/cmd/bootstrapper/run.go
Normal file
67
bootstrapper/cmd/bootstrapper/run.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/initserver"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/joinclient"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/logging"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/nodelock"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/edgelesssys/constellation/internal/grpc/dialer"
|
||||
"github.com/edgelesssys/constellation/internal/oid"
|
||||
"github.com/spf13/afero"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var version = "0.0.0"
|
||||
|
||||
func run(issuer quoteIssuer, tpm vtpm.TPMOpenFunc, fileHandler file.Handler,
|
||||
kube clusterInitJoiner, metadata joinclient.MetadataAPI,
|
||||
bindIP, bindPort string, logger *zap.Logger,
|
||||
cloudLogger logging.CloudLogger, fs afero.Fs,
|
||||
) {
|
||||
defer logger.Sync()
|
||||
logger.Info("starting bootstrapper", zap.String("version", version))
|
||||
|
||||
defer cloudLogger.Close()
|
||||
cloudLogger.Disclose("bootstrapper started running...")
|
||||
|
||||
nodeActivated, err := vtpm.IsNodeInitialized(tpm)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to check for previous activation using vTPM", zap.Error(err))
|
||||
}
|
||||
|
||||
if nodeActivated {
|
||||
if err := kube.StartKubelet(); err != nil {
|
||||
logger.Fatal("failed to restart kubelet", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nodeLock := nodelock.New()
|
||||
initServer := initserver.New(nodeLock, kube, logger)
|
||||
|
||||
dialer := dialer.New(issuer, nil, &net.Dialer{})
|
||||
joinClient := joinclient.New(nodeLock, dialer, kube, metadata, logger)
|
||||
|
||||
joinClient.Start()
|
||||
defer joinClient.Stop()
|
||||
|
||||
if err := initServer.Serve(bindIP, bindPort); err != nil {
|
||||
logger.Error("Failed to serve init server", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
type clusterInitJoiner interface {
|
||||
joinclient.ClusterJoiner
|
||||
initserver.ClusterInitializer
|
||||
StartKubelet() error
|
||||
}
|
||||
|
||||
type quoteIssuer interface {
|
||||
oid.Getter
|
||||
// Issue issues a quote for remote attestation for a given message
|
||||
Issue(userData []byte, nonce []byte) (quote []byte, err error)
|
||||
}
|
||||
58
bootstrapper/cmd/bootstrapper/test.go
Normal file
58
bootstrapper/cmd/bootstrapper/test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
attestationtypes "github.com/edgelesssys/constellation/internal/attestation/types"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
// ClusterFake behaves like a real cluster, but does not actually initialize or join Kubernetes.
|
||||
type clusterFake struct{}
|
||||
|
||||
// InitCluster fakes bootstrapping a new cluster with the current node being the master, returning the arguments required to join the cluster.
|
||||
func (c *clusterFake) InitCluster(context.Context, []string, string, string, attestationtypes.ID, kubernetes.KMSConfig, map[string]string,
|
||||
) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
// JoinCluster will fake joining the current node to an existing cluster.
|
||||
func (c *clusterFake) JoinCluster(context.Context, *kubeadm.BootstrapTokenDiscovery, string, role.Role) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartKubelet starts the kubelet service.
|
||||
func (c *clusterFake) StartKubelet() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type providerMetadataFake struct{}
|
||||
|
||||
func (f *providerMetadataFake) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
self, err := f.Self(ctx)
|
||||
return []metadata.InstanceMetadata{self}, err
|
||||
}
|
||||
|
||||
func (f *providerMetadataFake) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||
return metadata.InstanceMetadata{
|
||||
Name: "instanceName",
|
||||
ProviderID: "fake://instance-id",
|
||||
Role: role.Unknown,
|
||||
PrivateIPs: []string{"192.0.2.1"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *providerMetadataFake) SignalRole(ctx context.Context, role role.Role) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *providerMetadataFake) SetVPNIP(ctx context.Context, vpnIP string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *providerMetadataFake) Supported() bool {
|
||||
return true
|
||||
}
|
||||
394
bootstrapper/initproto/init.pb.go
Normal file
394
bootstrapper/initproto/init.pb.go
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.20.1
|
||||
// source: init.proto
|
||||
|
||||
package initproto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type InitRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
AutoscalingNodeGroups []string `protobuf:"bytes,1,rep,name=autoscaling_node_groups,json=autoscalingNodeGroups,proto3" json:"autoscaling_node_groups,omitempty"`
|
||||
MasterSecret []byte `protobuf:"bytes,2,opt,name=master_secret,json=masterSecret,proto3" json:"master_secret,omitempty"`
|
||||
KmsUri string `protobuf:"bytes,3,opt,name=kms_uri,json=kmsUri,proto3" json:"kms_uri,omitempty"`
|
||||
StorageUri string `protobuf:"bytes,4,opt,name=storage_uri,json=storageUri,proto3" json:"storage_uri,omitempty"`
|
||||
KeyEncryptionKeyId string `protobuf:"bytes,5,opt,name=key_encryption_key_id,json=keyEncryptionKeyId,proto3" json:"key_encryption_key_id,omitempty"`
|
||||
UseExistingKek bool `protobuf:"varint,6,opt,name=use_existing_kek,json=useExistingKek,proto3" json:"use_existing_kek,omitempty"`
|
||||
CloudServiceAccountUri string `protobuf:"bytes,7,opt,name=cloud_service_account_uri,json=cloudServiceAccountUri,proto3" json:"cloud_service_account_uri,omitempty"`
|
||||
KubernetesVersion string `protobuf:"bytes,8,opt,name=kubernetes_version,json=kubernetesVersion,proto3" json:"kubernetes_version,omitempty"`
|
||||
SshUserKeys []*SSHUserKey `protobuf:"bytes,9,rep,name=ssh_user_keys,json=sshUserKeys,proto3" json:"ssh_user_keys,omitempty"`
|
||||
}
|
||||
|
||||
func (x *InitRequest) Reset() {
|
||||
*x = InitRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_init_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *InitRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*InitRequest) ProtoMessage() {}
|
||||
|
||||
func (x *InitRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_init_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead.
|
||||
func (*InitRequest) Descriptor() ([]byte, []int) {
|
||||
return file_init_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetAutoscalingNodeGroups() []string {
|
||||
if x != nil {
|
||||
return x.AutoscalingNodeGroups
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetMasterSecret() []byte {
|
||||
if x != nil {
|
||||
return x.MasterSecret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetKmsUri() string {
|
||||
if x != nil {
|
||||
return x.KmsUri
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetStorageUri() string {
|
||||
if x != nil {
|
||||
return x.StorageUri
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetKeyEncryptionKeyId() string {
|
||||
if x != nil {
|
||||
return x.KeyEncryptionKeyId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetUseExistingKek() bool {
|
||||
if x != nil {
|
||||
return x.UseExistingKek
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetCloudServiceAccountUri() string {
|
||||
if x != nil {
|
||||
return x.CloudServiceAccountUri
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetKubernetesVersion() string {
|
||||
if x != nil {
|
||||
return x.KubernetesVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InitRequest) GetSshUserKeys() []*SSHUserKey {
|
||||
if x != nil {
|
||||
return x.SshUserKeys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InitResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Kubeconfig []byte `protobuf:"bytes,1,opt,name=kubeconfig,proto3" json:"kubeconfig,omitempty"`
|
||||
OwnerId []byte `protobuf:"bytes,2,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"`
|
||||
ClusterId []byte `protobuf:"bytes,3,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"`
|
||||
}
|
||||
|
||||
func (x *InitResponse) Reset() {
|
||||
*x = InitResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_init_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *InitResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*InitResponse) ProtoMessage() {}
|
||||
|
||||
func (x *InitResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_init_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead.
|
||||
func (*InitResponse) Descriptor() ([]byte, []int) {
|
||||
return file_init_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *InitResponse) GetKubeconfig() []byte {
|
||||
if x != nil {
|
||||
return x.Kubeconfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *InitResponse) GetOwnerId() []byte {
|
||||
if x != nil {
|
||||
return x.OwnerId
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *InitResponse) GetClusterId() []byte {
|
||||
if x != nil {
|
||||
return x.ClusterId
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SSHUserKey struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SSHUserKey) Reset() {
|
||||
*x = SSHUserKey{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_init_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SSHUserKey) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SSHUserKey) ProtoMessage() {}
|
||||
|
||||
func (x *SSHUserKey) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_init_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SSHUserKey.ProtoReflect.Descriptor instead.
|
||||
func (*SSHUserKey) Descriptor() ([]byte, []int) {
|
||||
return file_init_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *SSHUserKey) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SSHUserKey) GetPublicKey() string {
|
||||
if x != nil {
|
||||
return x.PublicKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_init_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_init_proto_rawDesc = []byte{
|
||||
0x0a, 0x0a, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x69, 0x6e,
|
||||
0x69, 0x74, 0x22, 0xa1, 0x03, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x61, 0x75, 0x74, 0x6f, 0x73, 0x63, 0x61, 0x6c, 0x69, 0x6e,
|
||||
0x67, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20,
|
||||
0x03, 0x28, 0x09, 0x52, 0x15, 0x61, 0x75, 0x74, 0x6f, 0x73, 0x63, 0x61, 0x6c, 0x69, 0x6e, 0x67,
|
||||
0x4e, 0x6f, 0x64, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61,
|
||||
0x73, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12,
|
||||
0x17, 0x0a, 0x07, 0x6b, 0x6d, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x06, 0x6b, 0x6d, 0x73, 0x55, 0x72, 0x69, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x6f, 0x72,
|
||||
0x61, 0x67, 0x65, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73,
|
||||
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x55, 0x72, 0x69, 0x12, 0x31, 0x0a, 0x15, 0x6b, 0x65, 0x79,
|
||||
0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6b, 0x65, 0x79, 0x45, 0x6e, 0x63,
|
||||
0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10,
|
||||
0x75, 0x73, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x6b,
|
||||
0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x75, 0x73, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74,
|
||||
0x69, 0x6e, 0x67, 0x4b, 0x65, 0x6b, 0x12, 0x39, 0x0a, 0x19, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f,
|
||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f,
|
||||
0x75, 0x72, 0x69, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x6c, 0x6f, 0x75, 0x64,
|
||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x72,
|
||||
0x69, 0x12, 0x2d, 0x0a, 0x12, 0x6b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x5f,
|
||||
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6b,
|
||||
0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x12, 0x34, 0x0a, 0x0d, 0x73, 0x73, 0x68, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79,
|
||||
0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x53,
|
||||
0x53, 0x48, 0x55, 0x73, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x73, 0x73, 0x68, 0x55, 0x73,
|
||||
0x65, 0x72, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x68, 0x0a, 0x0c, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, 0x62, 0x65,
|
||||
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49,
|
||||
0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64,
|
||||
0x22, 0x47, 0x0a, 0x0a, 0x53, 0x53, 0x48, 0x55, 0x73, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x1a,
|
||||
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75,
|
||||
0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
||||
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x32, 0x34, 0x0a, 0x03, 0x41, 0x50, 0x49,
|
||||
0x12, 0x2d, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e,
|
||||
0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x69, 0x6e,
|
||||
0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
|
||||
0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x64,
|
||||
0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x65,
|
||||
0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61,
|
||||
0x70, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x69, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_init_proto_rawDescOnce sync.Once
|
||||
file_init_proto_rawDescData = file_init_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_init_proto_rawDescGZIP() []byte {
|
||||
file_init_proto_rawDescOnce.Do(func() {
|
||||
file_init_proto_rawDescData = protoimpl.X.CompressGZIP(file_init_proto_rawDescData)
|
||||
})
|
||||
return file_init_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_init_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_init_proto_goTypes = []interface{}{
|
||||
(*InitRequest)(nil), // 0: init.InitRequest
|
||||
(*InitResponse)(nil), // 1: init.InitResponse
|
||||
(*SSHUserKey)(nil), // 2: init.SSHUserKey
|
||||
}
|
||||
var file_init_proto_depIdxs = []int32{
|
||||
2, // 0: init.InitRequest.ssh_user_keys:type_name -> init.SSHUserKey
|
||||
0, // 1: init.API.Init:input_type -> init.InitRequest
|
||||
1, // 2: init.API.Init:output_type -> init.InitResponse
|
||||
2, // [2:3] is the sub-list for method output_type
|
||||
1, // [1:2] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_init_proto_init() }
|
||||
func file_init_proto_init() {
|
||||
if File_init_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_init_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*InitRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_init_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*InitResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_init_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SSHUserKey); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_init_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_init_proto_goTypes,
|
||||
DependencyIndexes: file_init_proto_depIdxs,
|
||||
MessageInfos: file_init_proto_msgTypes,
|
||||
}.Build()
|
||||
File_init_proto = out.File
|
||||
file_init_proto_rawDesc = nil
|
||||
file_init_proto_goTypes = nil
|
||||
file_init_proto_depIdxs = nil
|
||||
}
|
||||
32
bootstrapper/initproto/init.proto
Normal file
32
bootstrapper/initproto/init.proto
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package init;
|
||||
|
||||
option go_package = "github.com/edgelesssys/constellation/bootstrapper/initproto";
|
||||
|
||||
service API {
|
||||
rpc Init(InitRequest) returns (InitResponse);
|
||||
}
|
||||
|
||||
message InitRequest {
|
||||
repeated string autoscaling_node_groups = 1;
|
||||
bytes master_secret = 2;
|
||||
string kms_uri = 3;
|
||||
string storage_uri = 4;
|
||||
string key_encryption_key_id = 5;
|
||||
bool use_existing_kek = 6;
|
||||
string cloud_service_account_uri = 7;
|
||||
string kubernetes_version = 8;
|
||||
repeated SSHUserKey ssh_user_keys = 9;
|
||||
}
|
||||
|
||||
message InitResponse {
|
||||
bytes kubeconfig = 1;
|
||||
bytes owner_id = 2;
|
||||
bytes cluster_id = 3;
|
||||
}
|
||||
|
||||
message SSHUserKey {
|
||||
string username = 1;
|
||||
string public_key = 2;
|
||||
}
|
||||
105
bootstrapper/initproto/init_grpc.pb.go
Normal file
105
bootstrapper/initproto/init_grpc.pb.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.20.1
|
||||
// source: init.proto
|
||||
|
||||
package initproto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// APIClient is the client API for API service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type APIClient interface {
|
||||
Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error)
|
||||
}
|
||||
|
||||
type aPIClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAPIClient(cc grpc.ClientConnInterface) APIClient {
|
||||
return &aPIClient{cc}
|
||||
}
|
||||
|
||||
func (c *aPIClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) {
|
||||
out := new(InitResponse)
|
||||
err := c.cc.Invoke(ctx, "/init.API/Init", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// APIServer is the server API for API service.
|
||||
// All implementations must embed UnimplementedAPIServer
|
||||
// for forward compatibility
|
||||
type APIServer interface {
|
||||
Init(context.Context, *InitRequest) (*InitResponse, error)
|
||||
mustEmbedUnimplementedAPIServer()
|
||||
}
|
||||
|
||||
// UnimplementedAPIServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedAPIServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedAPIServer) Init(context.Context, *InitRequest) (*InitResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Init not implemented")
|
||||
}
|
||||
func (UnimplementedAPIServer) mustEmbedUnimplementedAPIServer() {}
|
||||
|
||||
// UnsafeAPIServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to APIServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAPIServer interface {
|
||||
mustEmbedUnimplementedAPIServer()
|
||||
}
|
||||
|
||||
func RegisterAPIServer(s grpc.ServiceRegistrar, srv APIServer) {
|
||||
s.RegisterService(&API_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _API_Init_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InitRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(APIServer).Init(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/init.API/Init",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(APIServer).Init(ctx, req.(*InitRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// API_ServiceDesc is the grpc.ServiceDesc for API service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var API_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "init.API",
|
||||
HandlerType: (*APIServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Init",
|
||||
Handler: _API_Init_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "init.proto",
|
||||
}
|
||||
119
bootstrapper/internal/diskencryption/diskencryption.go
Normal file
119
bootstrapper/internal/diskencryption/diskencryption.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package diskencryption
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/martinjungblut/go-cryptsetup"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
stateMapperDevice = "state"
|
||||
initialKeyPath = "/run/cryptsetup-keys.d/state.key"
|
||||
keyslot = 0
|
||||
)
|
||||
|
||||
var (
|
||||
// packageLock is needed to block concurrent use of package functions, since libcryptsetup is not thread safe.
|
||||
// See: https://gitlab.com/cryptsetup/cryptsetup/-/issues/710
|
||||
// https://stackoverflow.com/questions/30553386/cryptsetup-backend-safe-with-multithreading
|
||||
packageLock = sync.Mutex{}
|
||||
errDeviceNotOpen = errors.New("cryptdevice not open")
|
||||
errDeviceAlreadyOpen = errors.New("cryptdevice already open")
|
||||
)
|
||||
|
||||
// Cryptsetup manages the encrypted state mapper device.
|
||||
type Cryptsetup struct {
|
||||
fs afero.Fs
|
||||
device cryptdevice
|
||||
initByName initByName
|
||||
}
|
||||
|
||||
// New creates a new Cryptsetup.
|
||||
func New() *Cryptsetup {
|
||||
return &Cryptsetup{
|
||||
fs: afero.NewOsFs(),
|
||||
initByName: func(name string) (cryptdevice, error) {
|
||||
return cryptsetup.InitByName(name)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens the cryptdevice.
|
||||
func (c *Cryptsetup) Open() error {
|
||||
packageLock.Lock()
|
||||
defer packageLock.Unlock()
|
||||
if c.device != nil {
|
||||
return errDeviceAlreadyOpen
|
||||
}
|
||||
var err error
|
||||
c.device, err = c.initByName(stateMapperDevice)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing crypt device for mapped device %q: %w", stateMapperDevice, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the cryptdevice.
|
||||
func (c *Cryptsetup) Close() error {
|
||||
packageLock.Lock()
|
||||
defer packageLock.Unlock()
|
||||
if c.device == nil {
|
||||
return errDeviceNotOpen
|
||||
}
|
||||
c.device.Free()
|
||||
c.device = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// UUID gets the device's UUID.
|
||||
// Only works after calling Open().
|
||||
func (c *Cryptsetup) UUID() (string, error) {
|
||||
packageLock.Lock()
|
||||
defer packageLock.Unlock()
|
||||
if c.device == nil {
|
||||
return "", errDeviceNotOpen
|
||||
}
|
||||
uuid := c.device.GetUUID()
|
||||
if uuid == "" {
|
||||
return "", fmt.Errorf("unable to get UUID for mapped device %q", stateMapperDevice)
|
||||
}
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// UpdatePassphrase switches the initial random passphrase of the mapped crypt device to a permanent passphrase.
|
||||
// Only works after calling Open().
|
||||
func (c *Cryptsetup) UpdatePassphrase(passphrase string) error {
|
||||
packageLock.Lock()
|
||||
defer packageLock.Unlock()
|
||||
if c.device == nil {
|
||||
return errDeviceNotOpen
|
||||
}
|
||||
initialPassphrase, err := c.getInitialPassphrase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.device.KeyslotChangeByPassphrase(keyslot, keyslot, initialPassphrase, passphrase); err != nil {
|
||||
return fmt.Errorf("changing passphrase for mapped device %q: %w", stateMapperDevice, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getInitialPassphrase retrieves the initial passphrase used on first boot.
|
||||
func (c *Cryptsetup) getInitialPassphrase() (string, error) {
|
||||
passphrase, err := afero.ReadFile(c.fs, initialKeyPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading first boot encryption passphrase from disk: %w", err)
|
||||
}
|
||||
return string(passphrase), nil
|
||||
}
|
||||
|
||||
type cryptdevice interface {
|
||||
GetUUID() string
|
||||
KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error
|
||||
Free() bool
|
||||
}
|
||||
|
||||
type initByName func(name string) (cryptdevice, error)
|
||||
201
bootstrapper/internal/diskencryption/diskencryption_test.go
Normal file
201
bootstrapper/internal/diskencryption/diskencryption_test.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
package diskencryption
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestOpenClose(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
initByNameErr error
|
||||
operations []string
|
||||
wantErr bool
|
||||
}{
|
||||
"open and close work": {
|
||||
operations: []string{"open", "close"},
|
||||
},
|
||||
"opening twice fails": {
|
||||
operations: []string{"open", "open"},
|
||||
wantErr: true,
|
||||
},
|
||||
"closing first fails": {
|
||||
operations: []string{"close"},
|
||||
wantErr: true,
|
||||
},
|
||||
"initByName failure detected": {
|
||||
initByNameErr: errors.New("initByNameErr"),
|
||||
operations: []string{"open"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
crypt := Cryptsetup{
|
||||
fs: afero.NewMemMapFs(),
|
||||
initByName: func(name string) (cryptdevice, error) {
|
||||
return &stubCryptdevice{}, tc.initByNameErr
|
||||
},
|
||||
}
|
||||
|
||||
err := executeOperations(&crypt, tc.operations)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
open bool
|
||||
wantUUID string
|
||||
wantErr bool
|
||||
}{
|
||||
"getting uuid works": {
|
||||
open: true,
|
||||
wantUUID: "uuid",
|
||||
},
|
||||
"getting uuid on closed device fails": {
|
||||
wantErr: true,
|
||||
},
|
||||
"empty uuid is detected": {
|
||||
open: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
crypt := Cryptsetup{
|
||||
fs: afero.NewMemMapFs(),
|
||||
initByName: func(name string) (cryptdevice, error) {
|
||||
return &stubCryptdevice{uuid: tc.wantUUID}, nil
|
||||
},
|
||||
}
|
||||
|
||||
if tc.open {
|
||||
require.NoError(crypt.Open())
|
||||
defer crypt.Close()
|
||||
}
|
||||
uuid, err := crypt.UUID()
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantUUID, uuid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePassphrase(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
writePassphrase bool
|
||||
open bool
|
||||
keyslotChangeByPassphraseErr error
|
||||
wantErr bool
|
||||
}{
|
||||
"updating passphrase works": {
|
||||
writePassphrase: true,
|
||||
open: true,
|
||||
},
|
||||
"updating passphrase on closed device fails": {
|
||||
wantErr: true,
|
||||
},
|
||||
"reading initial passphrase can fail": {
|
||||
open: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"changing keyslot passphrase can fail": {
|
||||
open: true,
|
||||
writePassphrase: true,
|
||||
keyslotChangeByPassphraseErr: errors.New("keyslotChangeByPassphraseErr"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
if tc.writePassphrase {
|
||||
require.NoError(fs.MkdirAll(path.Base(initialKeyPath), 0o777))
|
||||
require.NoError(afero.WriteFile(fs, initialKeyPath, []byte("key"), 0o777))
|
||||
}
|
||||
|
||||
crypt := Cryptsetup{
|
||||
fs: fs,
|
||||
initByName: func(name string) (cryptdevice, error) {
|
||||
return &stubCryptdevice{keyslotChangeErr: tc.keyslotChangeByPassphraseErr}, nil
|
||||
},
|
||||
}
|
||||
|
||||
if tc.open {
|
||||
require.NoError(crypt.Open())
|
||||
defer crypt.Close()
|
||||
}
|
||||
err := crypt.UpdatePassphrase("new-key")
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func executeOperations(crypt *Cryptsetup, operations []string) error {
|
||||
for _, operation := range operations {
|
||||
var err error
|
||||
switch operation {
|
||||
case "open":
|
||||
err = crypt.Open()
|
||||
case "close":
|
||||
err = crypt.Close()
|
||||
default:
|
||||
panic("unknown operation")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type stubCryptdevice struct {
|
||||
uuid string
|
||||
keyslotChangeErr error
|
||||
}
|
||||
|
||||
func (s *stubCryptdevice) GetUUID() string {
|
||||
return s.uuid
|
||||
}
|
||||
|
||||
func (s *stubCryptdevice) KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error {
|
||||
return s.keyslotChangeErr
|
||||
}
|
||||
|
||||
func (s *stubCryptdevice) Free() bool {
|
||||
return false
|
||||
}
|
||||
209
bootstrapper/internal/initserver/initserver.go
Normal file
209
bootstrapper/internal/initserver/initserver.go
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package initserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/initproto"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/diskencryption"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/nodelock"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/nodestate"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/util"
|
||||
attestationtypes "github.com/edgelesssys/constellation/internal/attestation/types"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
|
||||
grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Server is the initialization server, which is started on each node.
|
||||
// The server handles initialization calls from the CLI and initializes the
|
||||
// Kubernetes cluster.
|
||||
type Server struct {
|
||||
nodeLock *nodelock.Lock
|
||||
initializer ClusterInitializer
|
||||
disk encryptedDisk
|
||||
fileHandler file.Handler
|
||||
grpcServer serveStopper
|
||||
|
||||
logger *zap.Logger
|
||||
|
||||
initproto.UnimplementedAPIServer
|
||||
}
|
||||
|
||||
// New creates a new initialization server.
|
||||
func New(lock *nodelock.Lock, kube ClusterInitializer, logger *zap.Logger) *Server {
|
||||
logger = logger.Named("initServer")
|
||||
server := &Server{
|
||||
nodeLock: lock,
|
||||
disk: diskencryption.New(),
|
||||
initializer: kube,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
grpcLogger := logger.Named("gRPC")
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
|
||||
grpc_ctxtags.StreamServerInterceptor(),
|
||||
grpc_zap.StreamServerInterceptor(grpcLogger),
|
||||
)),
|
||||
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
|
||||
grpc_ctxtags.UnaryServerInterceptor(),
|
||||
grpc_zap.UnaryServerInterceptor(grpcLogger),
|
||||
)),
|
||||
)
|
||||
initproto.RegisterAPIServer(grpcServer, server)
|
||||
|
||||
server.grpcServer = grpcServer
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) Serve(ip, port string) error {
|
||||
lis, err := net.Listen("tcp", net.JoinHostPort(ip, port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen: %w", err)
|
||||
}
|
||||
|
||||
return s.grpcServer.Serve(lis)
|
||||
}
|
||||
|
||||
// Init initializes the cluster.
|
||||
func (s *Server) Init(ctx context.Context, req *initproto.InitRequest) (*initproto.InitResponse, error) {
|
||||
if ok := s.nodeLock.TryLockOnce(); !ok {
|
||||
// The join client seems to already have a connection to an
|
||||
// existing join service. At this point, any further call to
|
||||
// init does not make sense, so we just stop.
|
||||
//
|
||||
// The server stops itself after the current call is done.
|
||||
go s.grpcServer.GracefulStop()
|
||||
return nil, status.Error(codes.FailedPrecondition, "node is already being activated")
|
||||
}
|
||||
|
||||
id, err := s.deriveAttestationID(req.MasterSecret)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "%s", err)
|
||||
}
|
||||
|
||||
if err := s.setupDisk(req.MasterSecret); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "setting up disk: %s", err)
|
||||
}
|
||||
|
||||
state := nodestate.NodeState{
|
||||
Role: role.ControlPlane,
|
||||
OwnerID: id.Owner,
|
||||
ClusterID: id.Cluster,
|
||||
}
|
||||
if err := state.ToFile(s.fileHandler); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "persisting node state: %s", err)
|
||||
}
|
||||
|
||||
kubeconfig, err := s.initializer.InitCluster(ctx,
|
||||
req.AutoscalingNodeGroups,
|
||||
req.CloudServiceAccountUri,
|
||||
req.KubernetesVersion,
|
||||
id,
|
||||
kubernetes.KMSConfig{
|
||||
MasterSecret: req.MasterSecret,
|
||||
KMSURI: req.KmsUri,
|
||||
StorageURI: req.StorageUri,
|
||||
KeyEncryptionKeyID: req.KeyEncryptionKeyId,
|
||||
UseExistingKEK: req.UseExistingKek,
|
||||
},
|
||||
sshProtoKeysToMap(req.SshUserKeys),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "initializing cluster: %s", err)
|
||||
}
|
||||
|
||||
return &initproto.InitResponse{
|
||||
Kubeconfig: kubeconfig,
|
||||
OwnerId: id.Owner,
|
||||
ClusterId: id.Cluster,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) setupDisk(masterSecret []byte) error {
|
||||
if err := s.disk.Open(); err != nil {
|
||||
return fmt.Errorf("opening encrypted disk: %w", err)
|
||||
}
|
||||
defer s.disk.Close()
|
||||
|
||||
uuid, err := s.disk.UUID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving uuid of disk: %w", err)
|
||||
}
|
||||
uuid = strings.ToLower(uuid)
|
||||
|
||||
// TODO: Choose a way to salt the key derivation
|
||||
diskKey, err := util.DeriveKey(masterSecret, []byte("Constellation"), []byte("key"+uuid), 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.disk.UpdatePassphrase(string(diskKey))
|
||||
}
|
||||
|
||||
func (s *Server) deriveAttestationID(masterSecret []byte) (attestationtypes.ID, error) {
|
||||
clusterID, err := util.GenerateRandomBytes(constants.RNGLengthDefault)
|
||||
if err != nil {
|
||||
return attestationtypes.ID{}, err
|
||||
}
|
||||
|
||||
// TODO: Choose a way to salt the key derivation
|
||||
ownerID, err := util.DeriveKey(masterSecret, []byte("Constellation"), []byte("id"), constants.RNGLengthDefault)
|
||||
if err != nil {
|
||||
return attestationtypes.ID{}, err
|
||||
}
|
||||
|
||||
return attestationtypes.ID{Owner: ownerID, Cluster: clusterID}, nil
|
||||
}
|
||||
|
||||
func sshProtoKeysToMap(keys []*initproto.SSHUserKey) map[string]string {
|
||||
keyMap := make(map[string]string)
|
||||
for _, key := range keys {
|
||||
keyMap[key.Username] = key.PublicKey
|
||||
}
|
||||
return keyMap
|
||||
}
|
||||
|
||||
// ClusterInitializer has the ability to initialize a cluster.
|
||||
type ClusterInitializer interface {
|
||||
// InitCluster initializes a new Kubernetes cluster.
|
||||
InitCluster(
|
||||
ctx context.Context,
|
||||
autoscalingNodeGroups []string,
|
||||
cloudServiceAccountURI string,
|
||||
k8sVersion string,
|
||||
id attestationtypes.ID,
|
||||
kmsConfig kubernetes.KMSConfig,
|
||||
sshUserKeys map[string]string,
|
||||
) ([]byte, error)
|
||||
}
|
||||
|
||||
type encryptedDisk interface {
|
||||
// Open prepares the underlying device for disk operations.
|
||||
Open() error
|
||||
// Close closes the underlying device.
|
||||
Close() error
|
||||
// UUID gets the device's UUID.
|
||||
UUID() (string, error)
|
||||
// UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase.
|
||||
UpdatePassphrase(passphrase string) error
|
||||
}
|
||||
|
||||
type serveStopper interface {
|
||||
// Serve starts the server.
|
||||
Serve(lis net.Listener) error
|
||||
// GracefulStop stops the server and blocks until all requests are done.
|
||||
GracefulStop()
|
||||
}
|
||||
238
bootstrapper/internal/initserver/initserver_test.go
Normal file
238
bootstrapper/internal/initserver/initserver_test.go
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
package initserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/initproto"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/nodelock"
|
||||
attestationtypes "github.com/edgelesssys/constellation/internal/attestation/types"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
server := New(nodelock.New(), &stubClusterInitializer{}, zap.NewNop())
|
||||
assert.NotNil(server)
|
||||
assert.NotNil(server.logger)
|
||||
assert.NotNil(server.nodeLock)
|
||||
assert.NotNil(server.initializer)
|
||||
assert.NotNil(server.grpcServer)
|
||||
assert.NotNil(server.fileHandler)
|
||||
assert.NotNil(server.disk)
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
someErr := errors.New("failed")
|
||||
lockedNodeLock := nodelock.New()
|
||||
require.True(t, lockedNodeLock.TryLockOnce())
|
||||
|
||||
testCases := map[string]struct {
|
||||
nodeLock *nodelock.Lock
|
||||
initializer ClusterInitializer
|
||||
disk encryptedDisk
|
||||
fileHandler file.Handler
|
||||
req *initproto.InitRequest
|
||||
wantErr bool
|
||||
wantShutdown bool
|
||||
}{
|
||||
"successful init": {
|
||||
nodeLock: nodelock.New(),
|
||||
initializer: &stubClusterInitializer{},
|
||||
disk: &stubDisk{},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
req: &initproto.InitRequest{},
|
||||
},
|
||||
"node locked": {
|
||||
nodeLock: lockedNodeLock,
|
||||
initializer: &stubClusterInitializer{},
|
||||
disk: &stubDisk{},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
req: &initproto.InitRequest{},
|
||||
wantErr: true,
|
||||
wantShutdown: true,
|
||||
},
|
||||
"disk open error": {
|
||||
nodeLock: nodelock.New(),
|
||||
initializer: &stubClusterInitializer{},
|
||||
disk: &stubDisk{openErr: someErr},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
req: &initproto.InitRequest{},
|
||||
wantErr: true,
|
||||
},
|
||||
"disk uuid error": {
|
||||
nodeLock: nodelock.New(),
|
||||
initializer: &stubClusterInitializer{},
|
||||
disk: &stubDisk{uuidErr: someErr},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
req: &initproto.InitRequest{},
|
||||
wantErr: true,
|
||||
},
|
||||
"disk update passphrase error": {
|
||||
nodeLock: nodelock.New(),
|
||||
initializer: &stubClusterInitializer{},
|
||||
disk: &stubDisk{updatePassphraseErr: someErr},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
req: &initproto.InitRequest{},
|
||||
wantErr: true,
|
||||
},
|
||||
"write state file error": {
|
||||
nodeLock: nodelock.New(),
|
||||
initializer: &stubClusterInitializer{},
|
||||
disk: &stubDisk{},
|
||||
fileHandler: file.NewHandler(afero.NewReadOnlyFs(afero.NewMemMapFs())),
|
||||
req: &initproto.InitRequest{},
|
||||
wantErr: true,
|
||||
},
|
||||
"initialize cluster error": {
|
||||
nodeLock: nodelock.New(),
|
||||
initializer: &stubClusterInitializer{initClusterErr: someErr},
|
||||
disk: &stubDisk{},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
req: &initproto.InitRequest{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
serveStopper := newStubServeStopper()
|
||||
server := &Server{
|
||||
nodeLock: tc.nodeLock,
|
||||
initializer: tc.initializer,
|
||||
disk: tc.disk,
|
||||
fileHandler: tc.fileHandler,
|
||||
logger: zaptest.NewLogger(t),
|
||||
grpcServer: serveStopper,
|
||||
}
|
||||
|
||||
kubeconfig, err := server.Init(context.Background(), tc.req)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
|
||||
if tc.wantShutdown {
|
||||
select {
|
||||
case <-serveStopper.shutdownCalled:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("grpc server did not shut down")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(err)
|
||||
assert.NotNil(kubeconfig)
|
||||
assert.False(server.nodeLock.TryLockOnce()) // lock should be locked
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHProtoKeysToMap(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
keys []*initproto.SSHUserKey
|
||||
want map[string]string
|
||||
}{
|
||||
"empty": {
|
||||
keys: []*initproto.SSHUserKey{},
|
||||
want: map[string]string{},
|
||||
},
|
||||
"one key": {
|
||||
keys: []*initproto.SSHUserKey{
|
||||
{Username: "key1", PublicKey: "key1-key"},
|
||||
},
|
||||
want: map[string]string{
|
||||
"key1": "key1-key",
|
||||
},
|
||||
},
|
||||
"two keys": {
|
||||
keys: []*initproto.SSHUserKey{
|
||||
{Username: "key1", PublicKey: "key1-key"},
|
||||
{Username: "key2", PublicKey: "key2-key"},
|
||||
},
|
||||
want: map[string]string{
|
||||
"key1": "key1-key",
|
||||
"key2": "key2-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
got := sshProtoKeysToMap(tc.keys)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubDisk struct {
|
||||
openErr error
|
||||
closeErr error
|
||||
uuid string
|
||||
uuidErr error
|
||||
updatePassphraseErr error
|
||||
updatePassphraseCalled bool
|
||||
}
|
||||
|
||||
func (d *stubDisk) Open() error {
|
||||
return d.openErr
|
||||
}
|
||||
|
||||
func (d *stubDisk) Close() error {
|
||||
return d.closeErr
|
||||
}
|
||||
|
||||
func (d *stubDisk) UUID() (string, error) {
|
||||
return d.uuid, d.uuidErr
|
||||
}
|
||||
|
||||
func (d *stubDisk) UpdatePassphrase(string) error {
|
||||
d.updatePassphraseCalled = true
|
||||
return d.updatePassphraseErr
|
||||
}
|
||||
|
||||
type stubClusterInitializer struct {
|
||||
initClusterKubeconfig []byte
|
||||
initClusterErr error
|
||||
}
|
||||
|
||||
func (i *stubClusterInitializer) InitCluster(context.Context, []string, string, string, attestationtypes.ID, kubernetes.KMSConfig, map[string]string,
|
||||
) ([]byte, error) {
|
||||
return i.initClusterKubeconfig, i.initClusterErr
|
||||
}
|
||||
|
||||
type stubServeStopper struct {
|
||||
shutdownCalled chan struct{}
|
||||
}
|
||||
|
||||
func newStubServeStopper() *stubServeStopper {
|
||||
return &stubServeStopper{shutdownCalled: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
func (s *stubServeStopper) Serve(net.Listener) error {
|
||||
panic("should not be called in a test")
|
||||
}
|
||||
|
||||
func (s *stubServeStopper) GracefulStop() {
|
||||
s.shutdownCalled <- struct{}{}
|
||||
}
|
||||
400
bootstrapper/internal/joinclient/client.go
Normal file
400
bootstrapper/internal/joinclient/client.go
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
package joinclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/activation/activationproto"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/diskencryption"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/nodelock"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/nodestate"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/spf13/afero"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
const (
|
||||
interval = 30 * time.Second
|
||||
timeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// JoinClient is a client for self-activation of node.
|
||||
type JoinClient struct {
|
||||
nodeLock *nodelock.Lock
|
||||
diskUUID string
|
||||
nodeName string
|
||||
role role.Role
|
||||
disk encryptedDisk
|
||||
fileHandler file.Handler
|
||||
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
clock clock.WithTicker
|
||||
|
||||
dialer grpcDialer
|
||||
joiner ClusterJoiner
|
||||
metadataAPI MetadataAPI
|
||||
|
||||
log *zap.Logger
|
||||
|
||||
mux sync.Mutex
|
||||
stopC chan struct{}
|
||||
stopDone chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new SelfActivationClient.
|
||||
func New(lock *nodelock.Lock, dial grpcDialer, joiner ClusterJoiner, meta MetadataAPI, log *zap.Logger) *JoinClient {
|
||||
return &JoinClient{
|
||||
disk: diskencryption.New(),
|
||||
fileHandler: file.NewHandler(afero.NewOsFs()),
|
||||
timeout: timeout,
|
||||
interval: interval,
|
||||
clock: clock.RealClock{},
|
||||
dialer: dial,
|
||||
joiner: joiner,
|
||||
metadataAPI: meta,
|
||||
log: log.Named("selfactivation-client"),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the client routine. The client will make the needed API calls to activate
|
||||
// the node as the role it receives from the metadata API.
|
||||
// Multiple calls of start on the same client won't start a second routine if there is
|
||||
// already a routine running.
|
||||
func (c *JoinClient) Start() {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.stopC != nil { // daemon already running
|
||||
return
|
||||
}
|
||||
|
||||
c.log.Info("Starting")
|
||||
c.stopC = make(chan struct{}, 1)
|
||||
c.stopDone = make(chan struct{}, 1)
|
||||
|
||||
ticker := c.clock.NewTicker(c.interval)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
defer func() { c.stopDone <- struct{}{} }()
|
||||
defer c.log.Info("Client stopped")
|
||||
|
||||
diskUUID, err := c.getDiskUUID()
|
||||
if err != nil {
|
||||
c.log.Error("Failed to get disk UUID", zap.Error(err))
|
||||
return
|
||||
}
|
||||
c.diskUUID = diskUUID
|
||||
|
||||
for {
|
||||
err := c.getNodeMetadata()
|
||||
if err == nil {
|
||||
c.log.Info("Received own instance metadata", zap.String("role", c.role.String()), zap.String("name", c.nodeName))
|
||||
break
|
||||
}
|
||||
c.log.Info("Failed to retrieve instance metadata", zap.Error(err))
|
||||
|
||||
c.log.Info("Sleeping", zap.Duration("interval", c.interval))
|
||||
select {
|
||||
case <-c.stopC:
|
||||
return
|
||||
case <-ticker.C():
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
err := c.tryJoinAtAvailableServices()
|
||||
if err == nil {
|
||||
c.log.Info("Activated successfully. SelfActivationClient shut down.")
|
||||
return
|
||||
} else if isUnrecoverable(err) {
|
||||
c.log.Error("Unrecoverable error occurred", zap.Error(err))
|
||||
return
|
||||
}
|
||||
c.log.Info("Activation failed for all available endpoints", zap.Error(err))
|
||||
|
||||
c.log.Info("Sleeping", zap.Duration("interval", c.interval))
|
||||
select {
|
||||
case <-c.stopC:
|
||||
return
|
||||
case <-ticker.C():
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the client and blocks until the client's routine is stopped.
|
||||
func (c *JoinClient) Stop() {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.stopC == nil { // daemon not running
|
||||
return
|
||||
}
|
||||
|
||||
c.log.Info("Stopping")
|
||||
|
||||
c.stopC <- struct{}{}
|
||||
<-c.stopDone
|
||||
|
||||
c.stopC = nil
|
||||
c.stopDone = nil
|
||||
|
||||
c.log.Info("Stopped")
|
||||
}
|
||||
|
||||
func (c *JoinClient) tryJoinAtAvailableServices() error {
|
||||
ips, err := c.getControlPlaneIPs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return errors.New("no control plane IPs found")
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
err = c.join(net.JoinHostPort(ip, strconv.Itoa(constants.ActivationServiceNodePort)))
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *JoinClient) join(serviceEndpoint string) error {
|
||||
ctx, cancel := c.timeoutCtx()
|
||||
defer cancel()
|
||||
|
||||
conn, err := c.dialer.Dial(ctx, serviceEndpoint)
|
||||
if err != nil {
|
||||
c.log.Info("join service unreachable", zap.String("endpoint", serviceEndpoint), zap.Error(err))
|
||||
return fmt.Errorf("dialing join service endpoint: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
protoClient := activationproto.NewAPIClient(conn)
|
||||
|
||||
switch c.role {
|
||||
case role.Worker:
|
||||
return c.joinAsWorkerNode(ctx, protoClient)
|
||||
case role.ControlPlane:
|
||||
return c.joinAsControlPlaneNode(ctx, protoClient)
|
||||
default:
|
||||
return fmt.Errorf("cannot activate as %s", role.Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *JoinClient) joinAsWorkerNode(ctx context.Context, client activationproto.APIClient) error {
|
||||
req := &activationproto.ActivateWorkerNodeRequest{
|
||||
DiskUuid: c.diskUUID,
|
||||
NodeName: c.nodeName,
|
||||
}
|
||||
resp, err := client.ActivateWorkerNode(ctx, req)
|
||||
if err != nil {
|
||||
c.log.Info("Failed to activate as worker node", zap.Error(err))
|
||||
return fmt.Errorf("activating worker node: %w", err)
|
||||
}
|
||||
c.log.Info("Activation at AaaS succeeded")
|
||||
|
||||
return c.startNodeAndJoin(
|
||||
ctx,
|
||||
resp.StateDiskKey,
|
||||
resp.OwnerId,
|
||||
resp.ClusterId,
|
||||
resp.KubeletKey,
|
||||
resp.KubeletCert,
|
||||
resp.ApiServerEndpoint,
|
||||
resp.Token,
|
||||
resp.DiscoveryTokenCaCertHash,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
func (c *JoinClient) joinAsControlPlaneNode(ctx context.Context, client activationproto.APIClient) error {
|
||||
req := &activationproto.ActivateControlPlaneNodeRequest{
|
||||
DiskUuid: c.diskUUID,
|
||||
NodeName: c.nodeName,
|
||||
}
|
||||
resp, err := client.ActivateControlPlaneNode(ctx, req)
|
||||
if err != nil {
|
||||
c.log.Info("Failed to activate as control plane node", zap.Error(err))
|
||||
return fmt.Errorf("activating control plane node: %w", err)
|
||||
}
|
||||
c.log.Info("Activation at AaaS succeeded")
|
||||
|
||||
return c.startNodeAndJoin(
|
||||
ctx,
|
||||
resp.StateDiskKey,
|
||||
resp.OwnerId,
|
||||
resp.ClusterId,
|
||||
resp.KubeletKey,
|
||||
resp.KubeletCert,
|
||||
resp.ApiServerEndpoint,
|
||||
resp.Token,
|
||||
resp.DiscoveryTokenCaCertHash,
|
||||
resp.CertificateKey,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *JoinClient) startNodeAndJoin(ctx context.Context, diskKey, ownerID, clusterID, kubeletKey, kubeletCert []byte, endpoint, token,
|
||||
discoveryCACertHash, certKey string,
|
||||
) (retErr error) {
|
||||
// If an error occurs in this func, the client cannot continue.
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
retErr = unrecoverableError{retErr}
|
||||
}
|
||||
}()
|
||||
|
||||
if ok := c.nodeLock.TryLockOnce(); !ok {
|
||||
// There is already a cluster initialization in progress on
|
||||
// this node, so there is no need to also join the cluster,
|
||||
// as the initializing node is automatically part of the cluster.
|
||||
return errors.New("node is already being initialized")
|
||||
}
|
||||
|
||||
if err := c.updateDiskPassphrase(string(diskKey)); err != nil {
|
||||
return fmt.Errorf("updating disk passphrase: %w", err)
|
||||
}
|
||||
|
||||
state := nodestate.NodeState{
|
||||
Role: c.role,
|
||||
OwnerID: ownerID,
|
||||
ClusterID: clusterID,
|
||||
}
|
||||
if err := state.ToFile(c.fileHandler); err != nil {
|
||||
return fmt.Errorf("persisting node state: %w", err)
|
||||
}
|
||||
|
||||
btd := &kubeadm.BootstrapTokenDiscovery{
|
||||
APIServerEndpoint: endpoint,
|
||||
Token: token,
|
||||
CACertHashes: []string{discoveryCACertHash},
|
||||
}
|
||||
if err := c.joiner.JoinCluster(ctx, btd, certKey, c.role); err != nil {
|
||||
return fmt.Errorf("joining Kubernetes cluster: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *JoinClient) getNodeMetadata() error {
|
||||
ctx, cancel := c.timeoutCtx()
|
||||
defer cancel()
|
||||
|
||||
c.log.Info("Requesting node metadata from metadata API")
|
||||
inst, err := c.metadataAPI.Self(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.log.Info("Received node metadata", zap.Any("instance", inst))
|
||||
|
||||
if inst.Name == "" {
|
||||
return errors.New("got instance metadata with empty name")
|
||||
}
|
||||
|
||||
if inst.Role == role.Unknown {
|
||||
return errors.New("got instance metadata with unknown role")
|
||||
}
|
||||
|
||||
c.nodeName = inst.Name
|
||||
c.role = inst.Role
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *JoinClient) updateDiskPassphrase(passphrase string) error {
|
||||
if err := c.disk.Open(); err != nil {
|
||||
return fmt.Errorf("opening disk: %w", err)
|
||||
}
|
||||
defer c.disk.Close()
|
||||
return c.disk.UpdatePassphrase(passphrase)
|
||||
}
|
||||
|
||||
func (c *JoinClient) getDiskUUID() (string, error) {
|
||||
if err := c.disk.Open(); err != nil {
|
||||
return "", fmt.Errorf("opening disk: %w", err)
|
||||
}
|
||||
defer c.disk.Close()
|
||||
return c.disk.UUID()
|
||||
}
|
||||
|
||||
func (c *JoinClient) getControlPlaneIPs() ([]string, error) {
|
||||
ctx, cancel := c.timeoutCtx()
|
||||
defer cancel()
|
||||
|
||||
instances, err := c.metadataAPI.List(ctx)
|
||||
if err != nil {
|
||||
c.log.Error("Failed to list instances from metadata API", zap.Error(err))
|
||||
return nil, fmt.Errorf("listing instances from metadata API: %w", err)
|
||||
}
|
||||
|
||||
ips := []string{}
|
||||
for _, instance := range instances {
|
||||
if instance.Role == role.ControlPlane {
|
||||
ips = append(ips, instance.PrivateIPs...)
|
||||
}
|
||||
}
|
||||
|
||||
c.log.Info("Received control plane endpoints", zap.Strings("IPs", ips))
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func (c *JoinClient) timeoutCtx() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), c.timeout)
|
||||
}
|
||||
|
||||
type unrecoverableError struct{ error }
|
||||
|
||||
func isUnrecoverable(err error) bool {
|
||||
var ue *unrecoverableError
|
||||
ok := errors.As(err, &ue)
|
||||
return ok
|
||||
}
|
||||
|
||||
type grpcDialer interface {
|
||||
Dial(ctx context.Context, target string) (*grpc.ClientConn, error)
|
||||
}
|
||||
|
||||
// ClusterJoiner has the ability to join a new node to an existing cluster.
|
||||
type ClusterJoiner interface {
|
||||
// JoinCluster joins a new node to an existing cluster.
|
||||
JoinCluster(
|
||||
ctx context.Context,
|
||||
args *kubeadm.BootstrapTokenDiscovery,
|
||||
certKey string,
|
||||
peerRole role.Role,
|
||||
) error
|
||||
}
|
||||
|
||||
// MetadataAPI provides information about the instances.
|
||||
type MetadataAPI interface {
|
||||
// List retrieves all instances belonging to the current constellation.
|
||||
List(ctx context.Context) ([]metadata.InstanceMetadata, error)
|
||||
// Self retrieves the current instance.
|
||||
Self(ctx context.Context) (metadata.InstanceMetadata, error)
|
||||
}
|
||||
|
||||
type encryptedDisk interface {
|
||||
// Open prepares the underlying device for disk operations.
|
||||
Open() error
|
||||
// Close closes the underlying device.
|
||||
Close() error
|
||||
// UUID gets the device's UUID.
|
||||
UUID() (string, error)
|
||||
// UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase.
|
||||
UpdatePassphrase(passphrase string) error
|
||||
}
|
||||
345
bootstrapper/internal/joinclient/client_test.go
Normal file
345
bootstrapper/internal/joinclient/client_test.go
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
package joinclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/activation/activationproto"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/nodelock"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/edgelesssys/constellation/internal/grpc/atlscredentials"
|
||||
"github.com/edgelesssys/constellation/internal/grpc/dialer"
|
||||
"github.com/edgelesssys/constellation/internal/grpc/testdialer"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
"google.golang.org/grpc"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
testclock "k8s.io/utils/clock/testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
someErr := errors.New("failed")
|
||||
self := metadata.InstanceMetadata{Role: role.Worker, Name: "node-1"}
|
||||
peers := []metadata.InstanceMetadata{
|
||||
{Role: role.Worker, Name: "node-2", PrivateIPs: []string{"192.0.2.8"}},
|
||||
{Role: role.ControlPlane, Name: "node-3", PrivateIPs: []string{"192.0.2.1"}},
|
||||
{Role: role.ControlPlane, Name: "node-4", PrivateIPs: []string{"192.0.2.2", "192.0.2.3"}},
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
role role.Role
|
||||
clusterJoiner *stubClusterJoiner
|
||||
disk encryptedDisk
|
||||
nodeLock *nodelock.Lock
|
||||
apiAnswers []any
|
||||
}{
|
||||
"on worker: metadata self: errors occur": {
|
||||
role: role.Worker,
|
||||
apiAnswers: []any{
|
||||
selfAnswer{err: someErr},
|
||||
selfAnswer{err: someErr},
|
||||
selfAnswer{err: someErr},
|
||||
selfAnswer{instance: self},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{},
|
||||
},
|
||||
clusterJoiner: &stubClusterJoiner{},
|
||||
nodeLock: nodelock.New(),
|
||||
disk: &stubDisk{},
|
||||
},
|
||||
"on worker: metadata self: invalid answer": {
|
||||
role: role.Worker,
|
||||
apiAnswers: []any{
|
||||
selfAnswer{},
|
||||
selfAnswer{instance: metadata.InstanceMetadata{Role: role.Worker}},
|
||||
selfAnswer{instance: metadata.InstanceMetadata{Name: "node-1"}},
|
||||
selfAnswer{instance: self},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{},
|
||||
},
|
||||
clusterJoiner: &stubClusterJoiner{},
|
||||
nodeLock: nodelock.New(),
|
||||
disk: &stubDisk{},
|
||||
},
|
||||
"on worker: metadata list: errors occur": {
|
||||
role: role.Worker,
|
||||
apiAnswers: []any{
|
||||
selfAnswer{instance: self},
|
||||
listAnswer{err: someErr},
|
||||
listAnswer{err: someErr},
|
||||
listAnswer{err: someErr},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{},
|
||||
},
|
||||
clusterJoiner: &stubClusterJoiner{},
|
||||
nodeLock: nodelock.New(),
|
||||
disk: &stubDisk{},
|
||||
},
|
||||
"on worker: metadata list: no control plane nodes in answer": {
|
||||
role: role.Worker,
|
||||
apiAnswers: []any{
|
||||
selfAnswer{instance: self},
|
||||
listAnswer{},
|
||||
listAnswer{},
|
||||
listAnswer{},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{},
|
||||
},
|
||||
clusterJoiner: &stubClusterJoiner{},
|
||||
nodeLock: nodelock.New(),
|
||||
disk: &stubDisk{},
|
||||
},
|
||||
"on worker: aaas ActivateNode: errors": {
|
||||
role: role.Worker,
|
||||
apiAnswers: []any{
|
||||
selfAnswer{instance: self},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{err: someErr},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{err: someErr},
|
||||
listAnswer{instances: peers},
|
||||
activateWorkerNodeAnswer{},
|
||||
},
|
||||
clusterJoiner: &stubClusterJoiner{},
|
||||
nodeLock: nodelock.New(),
|
||||
disk: &stubDisk{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
clock := testclock.NewFakeClock(time.Now())
|
||||
metadataAPI := newStubMetadataAPI()
|
||||
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
||||
|
||||
netDialer := testdialer.NewBufconnDialer()
|
||||
dialer := dialer.New(nil, nil, netDialer)
|
||||
|
||||
client := &JoinClient{
|
||||
nodeLock: tc.nodeLock,
|
||||
timeout: 30 * time.Second,
|
||||
interval: time.Millisecond,
|
||||
dialer: dialer,
|
||||
disk: tc.disk,
|
||||
joiner: tc.clusterJoiner,
|
||||
fileHandler: fileHandler,
|
||||
metadataAPI: metadataAPI,
|
||||
clock: clock,
|
||||
log: zaptest.NewLogger(t),
|
||||
}
|
||||
|
||||
serverCreds := atlscredentials.New(nil, nil)
|
||||
activationServer := grpc.NewServer(grpc.Creds(serverCreds))
|
||||
activationAPI := newStubActivationServiceAPI()
|
||||
activationproto.RegisterAPIServer(activationServer, activationAPI)
|
||||
port := strconv.Itoa(constants.ActivationServiceNodePort)
|
||||
listener := netDialer.GetListener(net.JoinHostPort("192.0.2.3", port))
|
||||
go activationServer.Serve(listener)
|
||||
defer activationServer.GracefulStop()
|
||||
|
||||
client.Start()
|
||||
|
||||
for _, a := range tc.apiAnswers {
|
||||
switch a := a.(type) {
|
||||
case selfAnswer:
|
||||
metadataAPI.selfAnswerC <- a
|
||||
case listAnswer:
|
||||
metadataAPI.listAnswerC <- a
|
||||
case activateWorkerNodeAnswer:
|
||||
activationAPI.activateWorkerNodeAnswerC <- a
|
||||
}
|
||||
clock.Step(time.Second)
|
||||
}
|
||||
|
||||
client.Stop()
|
||||
|
||||
assert.True(tc.clusterJoiner.joinClusterCalled)
|
||||
assert.False(client.nodeLock.TryLockOnce()) // lock should be locked
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConcurrentStartStop(t *testing.T) {
|
||||
netDialer := testdialer.NewBufconnDialer()
|
||||
dialer := dialer.New(nil, nil, netDialer)
|
||||
client := &JoinClient{
|
||||
nodeLock: nodelock.New(),
|
||||
timeout: 30 * time.Second,
|
||||
interval: 30 * time.Second,
|
||||
dialer: dialer,
|
||||
disk: &stubDisk{},
|
||||
joiner: &stubClusterJoiner{},
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
metadataAPI: &stubRepeaterMetadataAPI{},
|
||||
clock: testclock.NewFakeClock(time.Now()),
|
||||
log: zap.NewNop(),
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
start := func() {
|
||||
defer wg.Done()
|
||||
client.Start()
|
||||
}
|
||||
|
||||
stop := func() {
|
||||
defer wg.Done()
|
||||
client.Stop()
|
||||
}
|
||||
|
||||
wg.Add(10)
|
||||
go stop()
|
||||
go start()
|
||||
go start()
|
||||
go stop()
|
||||
go stop()
|
||||
go start()
|
||||
go start()
|
||||
go stop()
|
||||
go stop()
|
||||
go start()
|
||||
wg.Wait()
|
||||
|
||||
client.Stop()
|
||||
}
|
||||
|
||||
type stubRepeaterMetadataAPI struct {
|
||||
selfInstance metadata.InstanceMetadata
|
||||
selfErr error
|
||||
listInstances []metadata.InstanceMetadata
|
||||
listErr error
|
||||
}
|
||||
|
||||
func (s *stubRepeaterMetadataAPI) Self(_ context.Context) (metadata.InstanceMetadata, error) {
|
||||
return s.selfInstance, s.selfErr
|
||||
}
|
||||
|
||||
func (s *stubRepeaterMetadataAPI) List(_ context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
return s.listInstances, s.listErr
|
||||
}
|
||||
|
||||
type stubMetadataAPI struct {
|
||||
selfAnswerC chan selfAnswer
|
||||
listAnswerC chan listAnswer
|
||||
}
|
||||
|
||||
func newStubMetadataAPI() *stubMetadataAPI {
|
||||
return &stubMetadataAPI{
|
||||
selfAnswerC: make(chan selfAnswer),
|
||||
listAnswerC: make(chan listAnswer),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stubMetadataAPI) Self(_ context.Context) (metadata.InstanceMetadata, error) {
|
||||
answer := <-s.selfAnswerC
|
||||
return answer.instance, answer.err
|
||||
}
|
||||
|
||||
func (s *stubMetadataAPI) List(_ context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
answer := <-s.listAnswerC
|
||||
return answer.instances, answer.err
|
||||
}
|
||||
|
||||
type selfAnswer struct {
|
||||
instance metadata.InstanceMetadata
|
||||
err error
|
||||
}
|
||||
|
||||
type listAnswer struct {
|
||||
instances []metadata.InstanceMetadata
|
||||
err error
|
||||
}
|
||||
|
||||
type stubActivationServiceAPI struct {
|
||||
activateWorkerNodeAnswerC chan activateWorkerNodeAnswer
|
||||
activateControlPlaneNodeAnswerC chan activateControlPlaneNodeAnswer
|
||||
|
||||
activationproto.UnimplementedAPIServer
|
||||
}
|
||||
|
||||
func newStubActivationServiceAPI() *stubActivationServiceAPI {
|
||||
return &stubActivationServiceAPI{
|
||||
activateWorkerNodeAnswerC: make(chan activateWorkerNodeAnswer),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stubActivationServiceAPI) ActivateWorkerNode(_ context.Context, _ *activationproto.ActivateWorkerNodeRequest,
|
||||
) (*activationproto.ActivateWorkerNodeResponse, error) {
|
||||
answer := <-s.activateWorkerNodeAnswerC
|
||||
if answer.resp == nil {
|
||||
answer.resp = &activationproto.ActivateWorkerNodeResponse{}
|
||||
}
|
||||
return answer.resp, answer.err
|
||||
}
|
||||
|
||||
func (s *stubActivationServiceAPI) ActivateControlPlaneNode(_ context.Context, _ *activationproto.ActivateControlPlaneNodeRequest,
|
||||
) (*activationproto.ActivateControlPlaneNodeResponse, error) {
|
||||
answer := <-s.activateControlPlaneNodeAnswerC
|
||||
if answer.resp == nil {
|
||||
answer.resp = &activationproto.ActivateControlPlaneNodeResponse{}
|
||||
}
|
||||
return answer.resp, answer.err
|
||||
}
|
||||
|
||||
type activateWorkerNodeAnswer struct {
|
||||
resp *activationproto.ActivateWorkerNodeResponse
|
||||
err error
|
||||
}
|
||||
|
||||
type activateControlPlaneNodeAnswer struct {
|
||||
resp *activationproto.ActivateControlPlaneNodeResponse
|
||||
err error
|
||||
}
|
||||
|
||||
type stubClusterJoiner struct {
|
||||
joinClusterCalled bool
|
||||
joinClusterErr error
|
||||
}
|
||||
|
||||
func (j *stubClusterJoiner) JoinCluster(context.Context, *kubeadm.BootstrapTokenDiscovery, string, role.Role) error {
|
||||
j.joinClusterCalled = true
|
||||
return j.joinClusterErr
|
||||
}
|
||||
|
||||
type stubDisk struct {
|
||||
openErr error
|
||||
closeErr error
|
||||
uuid string
|
||||
uuidErr error
|
||||
updatePassphraseErr error
|
||||
updatePassphraseCalled bool
|
||||
}
|
||||
|
||||
func (d *stubDisk) Open() error {
|
||||
return d.openErr
|
||||
}
|
||||
|
||||
func (d *stubDisk) Close() error {
|
||||
return d.closeErr
|
||||
}
|
||||
|
||||
func (d *stubDisk) UUID() (string, error) {
|
||||
return d.uuid, d.uuidErr
|
||||
}
|
||||
|
||||
func (d *stubDisk) UpdatePassphrase(string) error {
|
||||
d.updatePassphraseCalled = true
|
||||
return d.updatePassphraseErr
|
||||
}
|
||||
230
bootstrapper/internal/kubernetes/cloud_provider.go
Normal file
230
bootstrapper/internal/kubernetes/cloud_provider.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/cloud/metadata"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// ProviderMetadata implementers read/write cloud provider metadata.
|
||||
type ProviderMetadata interface {
|
||||
// List retrieves all instances belonging to the current Constellation.
|
||||
List(ctx context.Context) ([]metadata.InstanceMetadata, error)
|
||||
// Self retrieves the current instance.
|
||||
Self(ctx context.Context) (metadata.InstanceMetadata, error)
|
||||
// GetSubnetworkCIDR retrieves the subnetwork CIDR for the current instance.
|
||||
GetSubnetworkCIDR(ctx context.Context) (string, error)
|
||||
// SupportsLoadBalancer returns true if the cloud provider supports load balancers.
|
||||
SupportsLoadBalancer() bool
|
||||
// GetLoadBalancerIP retrieves the load balancer IP.
|
||||
GetLoadBalancerIP(ctx context.Context) (string, error)
|
||||
// GetInstance retrieves an instance using its providerID.
|
||||
GetInstance(ctx context.Context, providerID string) (metadata.InstanceMetadata, error)
|
||||
// Supported is used to determine if metadata API is implemented for this cloud provider.
|
||||
Supported() bool
|
||||
}
|
||||
|
||||
// CloudControllerManager implementers provide configuration for the k8s cloud-controller-manager.
|
||||
type CloudControllerManager interface {
|
||||
// Image returns the container image used to provide cloud-controller-manager for the cloud-provider.
|
||||
Image() string
|
||||
// Path returns the path used by cloud-controller-manager executable within the container image.
|
||||
Path() string
|
||||
// Name returns the cloud-provider name as used by k8s cloud-controller-manager (k8s.gcr.io/cloud-controller-manager).
|
||||
Name() string
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-controller-manager command.
|
||||
ExtraArgs() []string
|
||||
// ConfigMaps returns a list of ConfigMaps to deploy together with the k8s cloud-controller-manager
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/configmap/ .
|
||||
ConfigMaps(instance metadata.InstanceMetadata) (resources.ConfigMaps, error)
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/configuration/secret/ .
|
||||
Secrets(ctx context.Context, providerID, cloudServiceAccountURI string) (resources.Secrets, error)
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cloud-controller-manager.
|
||||
// Reference: https://kubernetes.io/docs/concepts/storage/volumes/ .
|
||||
Volumes() []k8s.Volume
|
||||
// VolumeMounts a list of of volume mounts to deploy together with the k8s cloud-controller-manager.
|
||||
VolumeMounts() []k8s.VolumeMount
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cloud-controller-manager.
|
||||
Env() []k8s.EnvVar
|
||||
// Supported is used to determine if cloud controller manager is implemented for this cloud provider.
|
||||
Supported() bool
|
||||
}
|
||||
|
||||
// CloudNodeManager implementers provide configuration for the k8s cloud-node-manager.
|
||||
type CloudNodeManager interface {
|
||||
// Image returns the container image used to provide cloud-node-manager for the cloud-provider.
|
||||
Image() string
|
||||
// Path returns the path used by cloud-node-manager executable within the container image.
|
||||
Path() string
|
||||
// ExtraArgs returns a list of arguments to append to the cloud-node-manager command.
|
||||
ExtraArgs() []string
|
||||
// Supported is used to determine if cloud node manager is implemented for this cloud provider.
|
||||
Supported() bool
|
||||
}
|
||||
|
||||
// ClusterAutoscaler implementers provide configuration for the k8s cluster-autoscaler.
|
||||
type ClusterAutoscaler interface {
|
||||
// Name returns the cloud-provider name as used by k8s cluster-autoscaler.
|
||||
Name() string
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cluster-autoscaler.
|
||||
Secrets(providerID, cloudServiceAccountURI string) (resources.Secrets, error)
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cluster-autoscaler.
|
||||
Volumes() []k8s.Volume
|
||||
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cluster-autoscaler.
|
||||
VolumeMounts() []k8s.VolumeMount
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cluster-autoscaler.
|
||||
Env() []k8s.EnvVar
|
||||
// Supported is used to determine if cluster autoscaler is implemented for this cloud provider.
|
||||
Supported() bool
|
||||
}
|
||||
|
||||
type stubProviderMetadata struct {
|
||||
GetLoadBalancerIPErr error
|
||||
GetLoadBalancerIPResp string
|
||||
|
||||
GetSubnetworkCIDRErr error
|
||||
GetSubnetworkCIDRResp string
|
||||
|
||||
ListErr error
|
||||
ListResp []metadata.InstanceMetadata
|
||||
|
||||
SelfErr error
|
||||
SelfResp metadata.InstanceMetadata
|
||||
|
||||
GetInstanceErr error
|
||||
GetInstanceResp metadata.InstanceMetadata
|
||||
|
||||
SupportedResp bool
|
||||
SupportsLoadBalancerResp bool
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) GetLoadBalancerIP(ctx context.Context) (string, error) {
|
||||
return m.GetLoadBalancerIPResp, m.GetLoadBalancerIPErr
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) GetSubnetworkCIDR(ctx context.Context) (string, error) {
|
||||
return m.GetSubnetworkCIDRResp, m.GetSubnetworkCIDRErr
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
||||
return m.ListResp, m.ListErr
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
||||
return m.SelfResp, m.SelfErr
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) GetInstance(ctx context.Context, providerID string) (metadata.InstanceMetadata, error) {
|
||||
return m.GetInstanceResp, m.GetInstanceErr
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) Supported() bool {
|
||||
return m.SupportedResp
|
||||
}
|
||||
|
||||
func (m *stubProviderMetadata) SupportsLoadBalancer() bool {
|
||||
return m.SupportsLoadBalancerResp
|
||||
}
|
||||
|
||||
type stubCloudControllerManager struct {
|
||||
SupportedResp bool
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Image() string {
|
||||
return "stub-image:latest"
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Path() string {
|
||||
return "/stub-controller-manager"
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Name() string {
|
||||
return "stub"
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) ExtraArgs() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) ConfigMaps(instance metadata.InstanceMetadata) (resources.ConfigMaps, error) {
|
||||
return []*k8s.ConfigMap{}, nil
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Secrets(ctx context.Context, instance, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
return []*k8s.Secret{}, nil
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{}
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{}
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{}
|
||||
}
|
||||
|
||||
func (m *stubCloudControllerManager) Supported() bool {
|
||||
return m.SupportedResp
|
||||
}
|
||||
|
||||
type stubCloudNodeManager struct {
|
||||
SupportedResp bool
|
||||
|
||||
ImageResp string
|
||||
PathResp string
|
||||
ExtraArgsResp []string
|
||||
}
|
||||
|
||||
func (m *stubCloudNodeManager) Image() string {
|
||||
return m.ImageResp
|
||||
}
|
||||
|
||||
func (m *stubCloudNodeManager) Path() string {
|
||||
return m.PathResp
|
||||
}
|
||||
|
||||
func (m *stubCloudNodeManager) ExtraArgs() []string {
|
||||
return m.ExtraArgsResp
|
||||
}
|
||||
|
||||
func (m *stubCloudNodeManager) Supported() bool {
|
||||
return m.SupportedResp
|
||||
}
|
||||
|
||||
type stubClusterAutoscaler struct {
|
||||
SupportedResp bool
|
||||
}
|
||||
|
||||
func (a *stubClusterAutoscaler) Name() string {
|
||||
return "stub"
|
||||
}
|
||||
|
||||
// Secrets returns a list of secrets to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *stubClusterAutoscaler) Secrets(instance, cloudServiceAccountURI string) (resources.Secrets, error) {
|
||||
return resources.Secrets{}, nil
|
||||
}
|
||||
|
||||
// Volumes returns a list of volumes to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *stubClusterAutoscaler) Volumes() []k8s.Volume {
|
||||
return []k8s.Volume{}
|
||||
}
|
||||
|
||||
// VolumeMounts returns a list of volume mounts to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *stubClusterAutoscaler) VolumeMounts() []k8s.VolumeMount {
|
||||
return []k8s.VolumeMount{}
|
||||
}
|
||||
|
||||
// Env returns a list of k8s environment key-value pairs to deploy together with the k8s cluster-autoscaler.
|
||||
func (a *stubClusterAutoscaler) Env() []k8s.EnvVar {
|
||||
return []k8s.EnvVar{}
|
||||
}
|
||||
|
||||
func (a *stubClusterAutoscaler) Supported() bool {
|
||||
return a.SupportedResp
|
||||
}
|
||||
216
bootstrapper/internal/kubernetes/k8sapi/install.go
Normal file
216
bootstrapper/internal/kubernetes/k8sapi/install.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// osInstaller installs binary components of supported kubernetes versions.
|
||||
type osInstaller struct {
|
||||
fs *afero.Afero
|
||||
hClient httpClient
|
||||
}
|
||||
|
||||
// newOSInstaller creates a new osInstaller.
|
||||
func newOSInstaller() *osInstaller {
|
||||
return &osInstaller{
|
||||
fs: &afero.Afero{Fs: afero.NewOsFs()},
|
||||
hClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// Install downloads a resource from a URL, applies any given text transformations and extracts the resulting file if required.
|
||||
// The resulting file(s) are copied to all destinations.
|
||||
func (i *osInstaller) Install(
|
||||
ctx context.Context, sourceURL string, destinations []string, perm fs.FileMode,
|
||||
extract bool, transforms ...transform.Transformer,
|
||||
) error {
|
||||
tempPath, err := i.downloadToTempDir(ctx, sourceURL, transforms...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = i.fs.Remove(tempPath)
|
||||
}()
|
||||
for _, destination := range destinations {
|
||||
var err error
|
||||
if extract {
|
||||
err = i.extractArchive(tempPath, destination, perm)
|
||||
} else {
|
||||
err = i.copy(tempPath, destination, perm)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("installing from %q: copying to destination %q: %w", sourceURL, destination, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractArchive extracts tar gz archives to a prefixed destination.
|
||||
func (i *osInstaller) extractArchive(archivePath, prefix string, perm fs.FileMode) error {
|
||||
archiveFile, err := i.fs.Open(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening archive file: %w", err)
|
||||
}
|
||||
defer archiveFile.Close()
|
||||
gzReader, err := gzip.NewReader(archiveFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading archive file as gzip: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
if err := i.fs.MkdirAll(prefix, fs.ModePerm); err != nil {
|
||||
return fmt.Errorf("creating prefix folder: %w", err)
|
||||
}
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing tar header: %w", err)
|
||||
}
|
||||
if err := verifyTarPath(header.Name); err != nil {
|
||||
return fmt.Errorf("verifying tar path %q: %w", header.Name, err)
|
||||
}
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if len(header.Name) == 0 {
|
||||
return errors.New("cannot create dir for empty path")
|
||||
}
|
||||
prefixedPath := path.Join(prefix, header.Name)
|
||||
if err := i.fs.Mkdir(prefixedPath, fs.FileMode(header.Mode)&perm); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return fmt.Errorf("creating folder %q: %w", prefixedPath, err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if len(header.Name) == 0 {
|
||||
return errors.New("cannot create file for empty path")
|
||||
}
|
||||
prefixedPath := path.Join(prefix, header.Name)
|
||||
out, err := i.fs.OpenFile(prefixedPath, os.O_WRONLY|os.O_CREATE, fs.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating file %q for writing: %w", prefixedPath, err)
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, tarReader); err != nil {
|
||||
return fmt.Errorf("writing extracted file contents: %w", err)
|
||||
}
|
||||
case tar.TypeSymlink:
|
||||
if err := verifyTarPath(header.Linkname); err != nil {
|
||||
return fmt.Errorf("invalid tar path %q: %w", header.Linkname, err)
|
||||
}
|
||||
if len(header.Name) == 0 {
|
||||
return errors.New("cannot symlink file for empty oldname")
|
||||
}
|
||||
if len(header.Linkname) == 0 {
|
||||
return errors.New("cannot symlink file for empty newname")
|
||||
}
|
||||
if symlinker, ok := i.fs.Fs.(afero.Symlinker); ok {
|
||||
if err := symlinker.SymlinkIfPossible(path.Join(prefix, header.Name), path.Join(prefix, header.Linkname)); err != nil {
|
||||
return fmt.Errorf("creating symlink: %w", err)
|
||||
}
|
||||
} else {
|
||||
return errors.New("fs does not support symlinks")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported tar record: %v", header.Typeflag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// downloadToTempDir downloads a file to a temporary location, applying transform on-the-fly.
|
||||
func (i *osInstaller) downloadToTempDir(ctx context.Context, url string, transforms ...transform.Transformer) (string, error) {
|
||||
out, err := afero.TempFile(i.fs, "", "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating destination temp file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request to download %q: %w", url, err)
|
||||
}
|
||||
resp, err := i.hClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request to download %q: %w", url, err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("request to download %q failed with status code: %v", url, resp.Status)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
transformReader := transform.NewReader(resp.Body, transform.Chain(transforms...))
|
||||
|
||||
if _, err = io.Copy(out, transformReader); err != nil {
|
||||
return "", fmt.Errorf("downloading %q: %w", url, err)
|
||||
}
|
||||
return out.Name(), nil
|
||||
}
|
||||
|
||||
// copy copies a file from oldname to newname.
|
||||
func (i *osInstaller) copy(oldname, newname string, perm fs.FileMode) (err error) {
|
||||
old, openOldErr := i.fs.OpenFile(oldname, os.O_RDONLY, fs.ModePerm)
|
||||
if openOldErr != nil {
|
||||
return fmt.Errorf("copying %q to %q: cannot open source file for reading: %w", oldname, newname, openOldErr)
|
||||
}
|
||||
defer func() { _ = old.Close() }()
|
||||
// create destination path if not exists
|
||||
if err := i.fs.MkdirAll(path.Dir(newname), fs.ModePerm); err != nil {
|
||||
return fmt.Errorf("copying %q to %q: unable to create destination folder: %w", oldname, newname, err)
|
||||
}
|
||||
new, openNewErr := i.fs.OpenFile(newname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perm)
|
||||
if openNewErr != nil {
|
||||
return fmt.Errorf("copying %q to %q: cannot open destination file for writing: %w", oldname, newname, openNewErr)
|
||||
}
|
||||
defer func() {
|
||||
_ = new.Close()
|
||||
if err != nil {
|
||||
_ = i.fs.Remove(newname)
|
||||
}
|
||||
}()
|
||||
if _, err := io.Copy(new, old); err != nil {
|
||||
return fmt.Errorf("copying %q to %q: copying file contents: %w", oldname, newname, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyTarPath checks if a tar path is valid (must not contain ".." as path element).
|
||||
func verifyTarPath(pat string) error {
|
||||
n := len(pat)
|
||||
r := 0
|
||||
for r < n {
|
||||
switch {
|
||||
case os.IsPathSeparator(pat[r]):
|
||||
// empty path element
|
||||
r++
|
||||
case pat[r] == '.' && (r+1 == n || os.IsPathSeparator(pat[r+1])):
|
||||
// . element
|
||||
r++
|
||||
case pat[r] == '.' && pat[r+1] == '.' && (r+2 == n || os.IsPathSeparator(pat[r+2])):
|
||||
// .. element
|
||||
return errors.New("path contains \"..\"")
|
||||
default:
|
||||
// skip to next path element
|
||||
for r < n && !os.IsPathSeparator(pat[r]) {
|
||||
r++
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
632
bootstrapper/internal/kubernetes/k8sapi/install_test.go
Normal file
632
bootstrapper/internal/kubernetes/k8sapi/install_test.go
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/icholy/replace"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/transform"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
server httpBufconnServer
|
||||
destination string
|
||||
extract bool
|
||||
transforms []transform.Transformer
|
||||
readonly bool
|
||||
wantErr bool
|
||||
wantFiles map[string][]byte
|
||||
}{
|
||||
"download works": {
|
||||
server: newHTTPBufconnServerWithBody([]byte("file-contents")),
|
||||
destination: "/destination",
|
||||
wantFiles: map[string][]byte{"/destination": []byte("file-contents")},
|
||||
},
|
||||
"download with extract works": {
|
||||
server: newHTTPBufconnServerWithBody(createTarGz([]byte("file-contents"), "/destination")),
|
||||
destination: "/prefix",
|
||||
extract: true,
|
||||
wantFiles: map[string][]byte{"/prefix/destination": []byte("file-contents")},
|
||||
},
|
||||
"download with transform works": {
|
||||
server: newHTTPBufconnServerWithBody([]byte("/usr/bin/kubelet")),
|
||||
destination: "/destination",
|
||||
transforms: []transform.Transformer{
|
||||
replace.String("/usr/bin", "/run/state/bin"),
|
||||
},
|
||||
wantFiles: map[string][]byte{"/destination": []byte("/run/state/bin/kubelet")},
|
||||
},
|
||||
"download fails": {
|
||||
server: newHTTPBufconnServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }),
|
||||
destination: "/destination",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
defer tc.server.Close()
|
||||
|
||||
hClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: tc.server.DialContext,
|
||||
Dial: tc.server.Dial,
|
||||
DialTLSContext: tc.server.DialContext,
|
||||
DialTLS: tc.server.Dial,
|
||||
},
|
||||
}
|
||||
|
||||
inst := osInstaller{
|
||||
fs: &afero.Afero{Fs: afero.NewMemMapFs()},
|
||||
hClient: &hClient,
|
||||
}
|
||||
err := inst.Install(context.Background(), "http://server/path", []string{tc.destination}, fs.ModePerm, tc.extract, tc.transforms...)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
for path, wantContents := range tc.wantFiles {
|
||||
contents, err := inst.fs.ReadFile(path)
|
||||
assert.NoError(err)
|
||||
assert.Equal(wantContents, contents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractArchive(t *testing.T) {
|
||||
tarGzTestFile := createTarGz([]byte("file-contents"), "/destination")
|
||||
tarGzTestWithFolder := createTarGzWithFolder([]byte("file-contents"), "/folder/destination", nil)
|
||||
|
||||
testCases := map[string]struct {
|
||||
source string
|
||||
destination string
|
||||
contents []byte
|
||||
readonly bool
|
||||
wantErr bool
|
||||
wantFiles map[string][]byte
|
||||
}{
|
||||
"extract works": {
|
||||
source: "in.tar.gz",
|
||||
destination: "/prefix",
|
||||
contents: tarGzTestFile,
|
||||
wantFiles: map[string][]byte{
|
||||
"/prefix/destination": []byte("file-contents"),
|
||||
},
|
||||
},
|
||||
"extract with folder works": {
|
||||
source: "in.tar.gz",
|
||||
destination: "/prefix",
|
||||
contents: tarGzTestWithFolder,
|
||||
wantFiles: map[string][]byte{
|
||||
"/prefix/folder/destination": []byte("file-contents"),
|
||||
},
|
||||
},
|
||||
"source missing": {
|
||||
source: "in.tar.gz",
|
||||
destination: "/prefix",
|
||||
wantErr: true,
|
||||
},
|
||||
"non-gzip file contents": {
|
||||
source: "in.tar.gz",
|
||||
contents: []byte("invalid bytes"),
|
||||
destination: "/prefix",
|
||||
wantErr: true,
|
||||
},
|
||||
"non-tar file contents": {
|
||||
source: "in.tar.gz",
|
||||
contents: createGz([]byte("file-contents")),
|
||||
destination: "/prefix",
|
||||
wantErr: true,
|
||||
},
|
||||
"mkdir prefix dir fails on RO fs": {
|
||||
source: "in.tar.gz",
|
||||
contents: tarGzTestFile,
|
||||
destination: "/prefix",
|
||||
readonly: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"mkdir tar dir fails on RO fs": {
|
||||
source: "in.tar.gz",
|
||||
contents: tarGzTestWithFolder,
|
||||
destination: "/",
|
||||
readonly: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"writing tar file fails on RO fs": {
|
||||
source: "in.tar.gz",
|
||||
contents: tarGzTestFile,
|
||||
destination: "/",
|
||||
readonly: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"symlink can be detected (but is unsupported on memmapfs)": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGzWithSymlink("source", "dest"),
|
||||
destination: "/prefix",
|
||||
wantErr: true,
|
||||
},
|
||||
"unsupported tar header type is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGzWithFifo("/destination"),
|
||||
destination: "/prefix",
|
||||
wantErr: true,
|
||||
},
|
||||
"path traversal is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGz([]byte{}, "../destination"),
|
||||
wantErr: true,
|
||||
},
|
||||
"path traversal in symlink is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGzWithSymlink("/source", "../destination"),
|
||||
wantErr: true,
|
||||
},
|
||||
"empty file name is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGz([]byte{}, ""),
|
||||
wantErr: true,
|
||||
},
|
||||
"empty folder name is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGzWithFolder([]byte{}, "source", stringPtr("")),
|
||||
wantErr: true,
|
||||
},
|
||||
"empty symlink source is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGzWithSymlink("", "/target"),
|
||||
wantErr: true,
|
||||
},
|
||||
"empty symlink target is detected": {
|
||||
source: "in.tar.gz",
|
||||
contents: createTarGzWithSymlink("/source", ""),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
afs := afero.NewMemMapFs()
|
||||
if len(tc.source) > 0 && len(tc.contents) > 0 {
|
||||
require.NoError(afero.WriteFile(afs, tc.source, tc.contents, fs.ModePerm))
|
||||
}
|
||||
|
||||
if tc.readonly {
|
||||
afs = afero.NewReadOnlyFs(afs)
|
||||
}
|
||||
|
||||
inst := osInstaller{
|
||||
fs: &afero.Afero{Fs: afs},
|
||||
}
|
||||
err := inst.extractArchive(tc.source, tc.destination, fs.ModePerm)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
for path, wantContents := range tc.wantFiles {
|
||||
contents, err := inst.fs.ReadFile(path)
|
||||
assert.NoError(err)
|
||||
assert.Equal(wantContents, contents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadToTempDir(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
server httpBufconnServer
|
||||
transforms []transform.Transformer
|
||||
readonly bool
|
||||
wantErr bool
|
||||
wantFile []byte
|
||||
}{
|
||||
"download works": {
|
||||
server: newHTTPBufconnServerWithBody([]byte("file-contents")),
|
||||
wantFile: []byte("file-contents"),
|
||||
},
|
||||
"download with transform works": {
|
||||
server: newHTTPBufconnServerWithBody([]byte("/usr/bin/kubelet")),
|
||||
transforms: []transform.Transformer{
|
||||
replace.String("/usr/bin", "/run/state/bin"),
|
||||
},
|
||||
wantFile: []byte("/run/state/bin/kubelet"),
|
||||
},
|
||||
"download fails": {
|
||||
server: newHTTPBufconnServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }),
|
||||
wantErr: true,
|
||||
},
|
||||
"creating temp file fails on RO fs": {
|
||||
server: newHTTPBufconnServerWithBody([]byte("file-contents")),
|
||||
readonly: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"content length mismatch": {
|
||||
server: newHTTPBufconnServer(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Content-Length", "1337")
|
||||
writer.WriteHeader(200)
|
||||
}),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
defer tc.server.Close()
|
||||
|
||||
hClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: tc.server.DialContext,
|
||||
Dial: tc.server.Dial,
|
||||
DialTLSContext: tc.server.DialContext,
|
||||
DialTLS: tc.server.Dial,
|
||||
},
|
||||
}
|
||||
|
||||
afs := afero.NewMemMapFs()
|
||||
if tc.readonly {
|
||||
afs = afero.NewReadOnlyFs(afs)
|
||||
}
|
||||
inst := osInstaller{
|
||||
fs: &afero.Afero{Fs: afs},
|
||||
hClient: &hClient,
|
||||
}
|
||||
path, err := inst.downloadToTempDir(context.Background(), "http://server/path", tc.transforms...)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
contents, err := inst.fs.ReadFile(path)
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantFile, contents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
contents := []byte("file-contents")
|
||||
existingFile := "/source"
|
||||
testCases := map[string]struct {
|
||||
oldname string
|
||||
newname string
|
||||
perm fs.FileMode
|
||||
readonly bool
|
||||
wantErr bool
|
||||
}{
|
||||
"copy works": {
|
||||
oldname: existingFile,
|
||||
newname: "/destination",
|
||||
perm: fs.ModePerm,
|
||||
},
|
||||
"oldname does not exist": {
|
||||
oldname: "missing",
|
||||
newname: "/destination",
|
||||
wantErr: true,
|
||||
},
|
||||
"copy on readonly fs fails": {
|
||||
oldname: existingFile,
|
||||
newname: "/destination",
|
||||
perm: fs.ModePerm,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
afs := afero.NewMemMapFs()
|
||||
require.NoError(afero.WriteFile(afs, existingFile, contents, fs.ModePerm))
|
||||
|
||||
if tc.readonly {
|
||||
afs = afero.NewReadOnlyFs(afs)
|
||||
}
|
||||
|
||||
inst := osInstaller{fs: &afero.Afero{Fs: afs}}
|
||||
err := inst.copy(tc.oldname, tc.newname, tc.perm)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
|
||||
oldfile, err := afs.Open(tc.oldname)
|
||||
assert.NoError(err)
|
||||
newfile, err := afs.Open(tc.newname)
|
||||
assert.NoError(err)
|
||||
|
||||
oldContents, _ := io.ReadAll(oldfile)
|
||||
newContents, _ := io.ReadAll(newfile)
|
||||
assert.Equal(oldContents, newContents)
|
||||
|
||||
newStat, _ := newfile.Stat()
|
||||
assert.Equal(tc.perm, newStat.Mode())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTarPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
path string
|
||||
wantErr bool
|
||||
}{
|
||||
"valid relative path": {
|
||||
path: "a/b/c",
|
||||
},
|
||||
"valid absolute path": {
|
||||
path: "/a/b/c",
|
||||
},
|
||||
"valid path with dot": {
|
||||
path: "/a/b/.d",
|
||||
},
|
||||
"valid path with dots": {
|
||||
path: "/a/b/..d",
|
||||
},
|
||||
"single dot in path is allowed": {
|
||||
path: ".",
|
||||
},
|
||||
"simple path traversal": {
|
||||
path: "..",
|
||||
wantErr: true,
|
||||
},
|
||||
"simple path traversal 2": {
|
||||
path: "../",
|
||||
wantErr: true,
|
||||
},
|
||||
"simple path traversal 3": {
|
||||
path: "/..",
|
||||
wantErr: true,
|
||||
},
|
||||
"simple path traversal 4": {
|
||||
path: "/../",
|
||||
wantErr: true,
|
||||
},
|
||||
"complex relative path traversal": {
|
||||
path: "a/b/c/../../../../c/d/e",
|
||||
wantErr: true,
|
||||
},
|
||||
"complex absolute path traversal": {
|
||||
path: "/a/b/c/../../../../c/d/e",
|
||||
wantErr: true,
|
||||
},
|
||||
"path traversal at the end": {
|
||||
path: "a/..",
|
||||
wantErr: true,
|
||||
},
|
||||
"path traversal at the end with trailing /": {
|
||||
path: "a/../",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
err := verifyTarPath(tc.path)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.path, path.Clean(tc.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type httpBufconnServer struct {
|
||||
*httptest.Server
|
||||
*bufconn.Listener
|
||||
}
|
||||
|
||||
func (s *httpBufconnServer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return s.Listener.DialContext(ctx)
|
||||
}
|
||||
|
||||
func (s *httpBufconnServer) Dial(network, addr string) (net.Conn, error) {
|
||||
return s.Listener.Dial()
|
||||
}
|
||||
|
||||
func (s *httpBufconnServer) Close() {
|
||||
s.Server.Close()
|
||||
s.Listener.Close()
|
||||
}
|
||||
|
||||
func newHTTPBufconnServer(handlerFunc http.HandlerFunc) httpBufconnServer {
|
||||
server := httptest.NewUnstartedServer(handlerFunc)
|
||||
listener := bufconn.Listen(1024)
|
||||
server.Listener = listener
|
||||
server.Start()
|
||||
return httpBufconnServer{
|
||||
Server: server,
|
||||
Listener: listener,
|
||||
}
|
||||
}
|
||||
|
||||
func newHTTPBufconnServerWithBody(body []byte) httpBufconnServer {
|
||||
return newHTTPBufconnServer(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if _, err := writer.Write(body); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createTarGz(contents []byte, path string) []byte {
|
||||
tgzWriter := newTarGzWriter()
|
||||
defer func() { _ = tgzWriter.Close() }()
|
||||
|
||||
if err := tgzWriter.writeHeader(&tar.Header{
|
||||
Typeflag: tar.TypeReg,
|
||||
Name: path,
|
||||
Size: int64(len(contents)),
|
||||
Mode: int64(fs.ModePerm),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := tgzWriter.writeTar(contents); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tgzWriter.Bytes()
|
||||
}
|
||||
|
||||
func createTarGzWithFolder(contents []byte, pat string, dirnameOverride *string) []byte {
|
||||
tgzWriter := newTarGzWriter()
|
||||
defer func() { _ = tgzWriter.Close() }()
|
||||
|
||||
dir := path.Dir(pat)
|
||||
if dirnameOverride != nil {
|
||||
dir = *dirnameOverride
|
||||
}
|
||||
|
||||
if err := tgzWriter.writeHeader(&tar.Header{
|
||||
Typeflag: tar.TypeDir,
|
||||
Name: dir,
|
||||
Mode: int64(fs.ModePerm),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := tgzWriter.writeHeader(&tar.Header{
|
||||
Typeflag: tar.TypeReg,
|
||||
Name: pat,
|
||||
Size: int64(len(contents)),
|
||||
Mode: int64(fs.ModePerm),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := tgzWriter.writeTar(contents); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tgzWriter.Bytes()
|
||||
}
|
||||
|
||||
func createTarGzWithSymlink(oldname, newname string) []byte {
|
||||
tgzWriter := newTarGzWriter()
|
||||
defer func() { _ = tgzWriter.Close() }()
|
||||
|
||||
if err := tgzWriter.writeHeader(&tar.Header{
|
||||
Typeflag: tar.TypeSymlink,
|
||||
Name: oldname,
|
||||
Linkname: newname,
|
||||
Mode: int64(fs.ModePerm),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tgzWriter.Bytes()
|
||||
}
|
||||
|
||||
func createTarGzWithFifo(name string) []byte {
|
||||
tgzWriter := newTarGzWriter()
|
||||
defer func() { _ = tgzWriter.Close() }()
|
||||
|
||||
if err := tgzWriter.writeHeader(&tar.Header{
|
||||
Typeflag: tar.TypeFifo,
|
||||
Name: name,
|
||||
Mode: int64(fs.ModePerm),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tgzWriter.Bytes()
|
||||
}
|
||||
|
||||
func createGz(contents []byte) []byte {
|
||||
tgzWriter := newTarGzWriter()
|
||||
defer func() { _ = tgzWriter.Close() }()
|
||||
|
||||
if _, err := tgzWriter.writeGz(contents); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tgzWriter.Bytes()
|
||||
}
|
||||
|
||||
type tarGzWriter struct {
|
||||
buf *bytes.Buffer
|
||||
bufWriter *bufio.Writer
|
||||
gzWriter *gzip.Writer
|
||||
tarWriter *tar.Writer
|
||||
}
|
||||
|
||||
func newTarGzWriter() *tarGzWriter {
|
||||
var buf bytes.Buffer
|
||||
bufWriter := bufio.NewWriter(&buf)
|
||||
gzipWriter := gzip.NewWriter(bufWriter)
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
|
||||
return &tarGzWriter{
|
||||
buf: &buf,
|
||||
bufWriter: bufWriter,
|
||||
gzWriter: gzipWriter,
|
||||
tarWriter: tarWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *tarGzWriter) writeHeader(hdr *tar.Header) error {
|
||||
return w.tarWriter.WriteHeader(hdr)
|
||||
}
|
||||
|
||||
func (w *tarGzWriter) writeTar(b []byte) (int, error) {
|
||||
return w.tarWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w *tarGzWriter) writeGz(b []byte) (int, error) {
|
||||
return w.gzWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w *tarGzWriter) Bytes() []byte {
|
||||
_ = w.tarWriter.Flush()
|
||||
_ = w.gzWriter.Flush()
|
||||
_ = w.gzWriter.Close() // required to ensure clean EOF in gz reader
|
||||
_ = w.bufWriter.Flush()
|
||||
return w.buf.Bytes()
|
||||
}
|
||||
|
||||
func (w *tarGzWriter) Close() (result error) {
|
||||
if err := w.tarWriter.Close(); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
if err := w.gzWriter.Close(); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
49
bootstrapper/internal/kubernetes/k8sapi/joinargs.go
Normal file
49
bootstrapper/internal/kubernetes/k8sapi/joinargs.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/shlex"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
func ParseJoinCommand(joinCommand string) (*kubeadm.BootstrapTokenDiscovery, error) {
|
||||
// Format:
|
||||
// kubeadm join [API_SERVER_ENDPOINT] --token [TOKEN] --discovery-token-ca-cert-hash [DISCOVERY_TOKEN_CA_CERT_HASH] --control-plane
|
||||
|
||||
// split and verify that this is a kubeadm join command
|
||||
argv, err := shlex.Split(joinCommand)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kubadm join command could not be tokenized: %v", joinCommand)
|
||||
}
|
||||
if len(argv) < 3 {
|
||||
return nil, fmt.Errorf("kubadm join command is too short: %v", argv)
|
||||
}
|
||||
if argv[0] != "kubeadm" || argv[1] != "join" {
|
||||
return nil, fmt.Errorf("not a kubeadm join command: %v", argv)
|
||||
}
|
||||
|
||||
result := kubeadm.BootstrapTokenDiscovery{APIServerEndpoint: argv[2]}
|
||||
|
||||
var caCertHash string
|
||||
// parse flags
|
||||
flags := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
flags.StringVar(&result.Token, "token", "", "")
|
||||
flags.StringVar(&caCertHash, "discovery-token-ca-cert-hash", "", "")
|
||||
flags.Bool("control-plane", false, "")
|
||||
flags.String("certificate-key", "", "")
|
||||
if err := flags.Parse(argv[3:]); err != nil {
|
||||
return nil, fmt.Errorf("parsing flag arguments: %v %w", argv, err)
|
||||
}
|
||||
|
||||
if result.Token == "" {
|
||||
return nil, fmt.Errorf("missing flag argument token: %v", argv)
|
||||
}
|
||||
if caCertHash == "" {
|
||||
return nil, fmt.Errorf("missing flag argument discovery-token-ca-cert-hash: %v", argv)
|
||||
}
|
||||
result.CACertHashes = []string{caCertHash}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
69
bootstrapper/internal/kubernetes/k8sapi/joinargs_test.go
Normal file
69
bootstrapper/internal/kubernetes/k8sapi/joinargs_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
func TestParseJoinCommand(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
joinCommand string
|
||||
wantJoinArgs kubeadm.BootstrapTokenDiscovery
|
||||
wantErr bool
|
||||
}{
|
||||
"join command can be parsed": {
|
||||
joinCommand: "kubeadm join 192.0.2.0:8443 --token dummy-token --discovery-token-ca-cert-hash sha512:dummy-hash --control-plane",
|
||||
wantJoinArgs: kubeadm.BootstrapTokenDiscovery{
|
||||
APIServerEndpoint: "192.0.2.0:8443",
|
||||
Token: "dummy-token",
|
||||
CACertHashes: []string{"sha512:dummy-hash"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"incorrect join command returns error": {
|
||||
joinCommand: "some string",
|
||||
wantErr: true,
|
||||
},
|
||||
"missing api server endpoint is checked": {
|
||||
joinCommand: "kubeadm join --token dummy-token --discovery-token-ca-cert-hash sha512:dummy-hash --control-plane",
|
||||
wantErr: true,
|
||||
},
|
||||
"missing token is checked": {
|
||||
joinCommand: "kubeadm join 192.0.2.0:8443 --discovery-token-ca-cert-hash sha512:dummy-hash --control-plane",
|
||||
wantErr: true,
|
||||
},
|
||||
"missing discovery-token-ca-cert-hash is checked": {
|
||||
joinCommand: "kubeadm join 192.0.2.0:8443 --token dummy-token --control-plane",
|
||||
wantErr: true,
|
||||
},
|
||||
"missing control-plane": {
|
||||
joinCommand: "kubeadm join 192.0.2.0:8443 --token dummy-token --discovery-token-ca-cert-hash sha512:dummy-hash",
|
||||
wantJoinArgs: kubeadm.BootstrapTokenDiscovery{
|
||||
APIServerEndpoint: "192.0.2.0:8443",
|
||||
Token: "dummy-token",
|
||||
CACertHashes: []string{"sha512:dummy-hash"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
joinArgs, err := ParseJoinCommand(tc.joinCommand)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(&tc.wantJoinArgs, joinArgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
289
bootstrapper/internal/kubernetes/k8sapi/kubeadm_config.go
Normal file
289
bootstrapper/internal/kubernetes/k8sapi/kubeadm_config.go
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubeletconf "k8s.io/kubelet/config/v1beta1"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
// Uses types defined here: https://kubernetes.io/docs/reference/config-api/kubeadm-config.v1beta3/
|
||||
// Slimmed down to the fields we require
|
||||
|
||||
const (
|
||||
bindPort = 6443
|
||||
auditLogDir = "/var/log/kubernetes/audit/"
|
||||
auditLogFile = "audit.log"
|
||||
auditPolicyPath = "/etc/kubernetes/audit-policy.yaml"
|
||||
)
|
||||
|
||||
type CoreOSConfiguration struct{}
|
||||
|
||||
func (c *CoreOSConfiguration) InitConfiguration(externalCloudProvider bool) KubeadmInitYAML {
|
||||
var cloudProvider string
|
||||
if externalCloudProvider {
|
||||
cloudProvider = "external"
|
||||
}
|
||||
return KubeadmInitYAML{
|
||||
InitConfiguration: kubeadm.InitConfiguration{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: kubeadm.SchemeGroupVersion.String(),
|
||||
Kind: "InitConfiguration",
|
||||
},
|
||||
NodeRegistration: kubeadm.NodeRegistrationOptions{
|
||||
CRISocket: "/run/containerd/containerd.sock",
|
||||
KubeletExtraArgs: map[string]string{
|
||||
"cloud-provider": cloudProvider,
|
||||
"network-plugin": "cni",
|
||||
},
|
||||
},
|
||||
// AdvertiseAddress will be overwritten later
|
||||
LocalAPIEndpoint: kubeadm.APIEndpoint{
|
||||
BindPort: bindPort,
|
||||
},
|
||||
},
|
||||
ClusterConfiguration: kubeadm.ClusterConfiguration{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ClusterConfiguration",
|
||||
APIVersion: kubeadm.SchemeGroupVersion.String(),
|
||||
},
|
||||
KubernetesVersion: constants.KubernetesVersion,
|
||||
// necessary to be able to access the kubeapi server through localhost
|
||||
APIServer: kubeadm.APIServer{
|
||||
ControlPlaneComponent: kubeadm.ControlPlaneComponent{
|
||||
ExtraArgs: map[string]string{
|
||||
"audit-policy-file": auditPolicyPath,
|
||||
"audit-log-path": filepath.Join(auditLogDir, auditLogFile), // CIS benchmark
|
||||
"audit-log-maxage": "30", // CIS benchmark - Default value of Rancher
|
||||
"audit-log-maxbackup": "10", // CIS benchmark - Default value of Rancher
|
||||
"audit-log-maxsize": "100", // CIS benchmark - Default value of Rancher
|
||||
"profiling": "false", // CIS benchmark
|
||||
"tls-cipher-suites": "TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256," +
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256," +
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384," +
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256," +
|
||||
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256," +
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305," +
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_RSA_WITH_3DES_EDE_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA," +
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_256_GCM_SHA384", // CIS benchmark
|
||||
},
|
||||
ExtraVolumes: []kubeadm.HostPathMount{
|
||||
{
|
||||
Name: "audit-log",
|
||||
HostPath: auditLogDir,
|
||||
MountPath: auditLogDir,
|
||||
ReadOnly: false,
|
||||
PathType: corev1.HostPathDirectoryOrCreate,
|
||||
},
|
||||
{
|
||||
Name: "audit",
|
||||
HostPath: auditPolicyPath,
|
||||
MountPath: auditPolicyPath,
|
||||
ReadOnly: true,
|
||||
PathType: corev1.HostPathFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
CertSANs: []string{"127.0.0.1", "10.118.0.1"},
|
||||
},
|
||||
ControllerManager: kubeadm.ControlPlaneComponent{
|
||||
ExtraArgs: map[string]string{
|
||||
"flex-volume-plugin-dir": "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/",
|
||||
"cloud-provider": cloudProvider,
|
||||
"configure-cloud-routes": "false",
|
||||
"profiling": "false", // CIS benchmark
|
||||
"terminated-pod-gc-threshold": "1000", // CIS benchmark - Default value of Rancher
|
||||
},
|
||||
},
|
||||
Scheduler: kubeadm.ControlPlaneComponent{
|
||||
ExtraArgs: map[string]string{
|
||||
"profiling": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
// warning: this config is applied to every node in the cluster!
|
||||
KubeletConfiguration: kubeletconf.KubeletConfiguration{
|
||||
ProtectKernelDefaults: true, // CIS benchmark
|
||||
TLSCipherSuites: []string{
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
||||
}, // CIS benchmark
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: kubeletconf.SchemeGroupVersion.String(),
|
||||
Kind: "KubeletConfiguration",
|
||||
},
|
||||
RegisterWithTaints: []corev1.Taint{
|
||||
{
|
||||
Key: "node.cloudprovider.kubernetes.io/uninitialized",
|
||||
Value: "true",
|
||||
Effect: corev1.TaintEffectPreferNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node.cilium.io/agent-not-ready",
|
||||
Value: "true",
|
||||
Effect: corev1.TaintEffectPreferNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CoreOSConfiguration) JoinConfiguration(externalCloudProvider bool) KubeadmJoinYAML {
|
||||
var cloudProvider string
|
||||
if externalCloudProvider {
|
||||
cloudProvider = "external"
|
||||
}
|
||||
return KubeadmJoinYAML{
|
||||
JoinConfiguration: kubeadm.JoinConfiguration{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: kubeadm.SchemeGroupVersion.String(),
|
||||
Kind: "JoinConfiguration",
|
||||
},
|
||||
NodeRegistration: kubeadm.NodeRegistrationOptions{
|
||||
CRISocket: "/run/containerd/containerd.sock",
|
||||
KubeletExtraArgs: map[string]string{
|
||||
"cloud-provider": cloudProvider,
|
||||
},
|
||||
},
|
||||
Discovery: kubeadm.Discovery{
|
||||
BootstrapToken: &kubeadm.BootstrapTokenDiscovery{},
|
||||
},
|
||||
},
|
||||
KubeletConfiguration: kubeletconf.KubeletConfiguration{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: kubeletconf.SchemeGroupVersion.String(),
|
||||
Kind: "KubeletConfiguration",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type KubeadmJoinYAML struct {
|
||||
JoinConfiguration kubeadm.JoinConfiguration
|
||||
KubeletConfiguration kubeletconf.KubeletConfiguration
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) SetNodeName(nodeName string) {
|
||||
k.JoinConfiguration.NodeRegistration.Name = nodeName
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) SetApiServerEndpoint(apiServerEndpoint string) {
|
||||
k.JoinConfiguration.Discovery.BootstrapToken.APIServerEndpoint = apiServerEndpoint
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) SetToken(token string) {
|
||||
k.JoinConfiguration.Discovery.BootstrapToken.Token = token
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) AppendDiscoveryTokenCaCertHash(discoveryTokenCaCertHash string) {
|
||||
k.JoinConfiguration.Discovery.BootstrapToken.CACertHashes = append(k.JoinConfiguration.Discovery.BootstrapToken.CACertHashes, discoveryTokenCaCertHash)
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) SetNodeIP(nodeIP string) {
|
||||
if k.JoinConfiguration.NodeRegistration.KubeletExtraArgs == nil {
|
||||
k.JoinConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{"node-ip": nodeIP}
|
||||
} else {
|
||||
k.JoinConfiguration.NodeRegistration.KubeletExtraArgs["node-ip"] = nodeIP
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) SetProviderID(providerID string) {
|
||||
k.KubeletConfiguration.ProviderID = providerID
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) SetControlPlane(advertiseAddress string, certificateKey string) {
|
||||
k.JoinConfiguration.ControlPlane = &kubeadm.JoinControlPlane{
|
||||
LocalAPIEndpoint: kubeadm.APIEndpoint{
|
||||
AdvertiseAddress: advertiseAddress,
|
||||
BindPort: 6443,
|
||||
},
|
||||
CertificateKey: certificateKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) Marshal() ([]byte, error) {
|
||||
return resources.MarshalK8SResources(k)
|
||||
}
|
||||
|
||||
func (k *KubeadmJoinYAML) Unmarshal(yamlData []byte) (KubeadmJoinYAML, error) {
|
||||
var tmp KubeadmJoinYAML
|
||||
return tmp, resources.UnmarshalK8SResources(yamlData, &tmp)
|
||||
}
|
||||
|
||||
type KubeadmInitYAML struct {
|
||||
InitConfiguration kubeadm.InitConfiguration
|
||||
ClusterConfiguration kubeadm.ClusterConfiguration
|
||||
KubeletConfiguration kubeletconf.KubeletConfiguration
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetNodeName(nodeName string) {
|
||||
k.InitConfiguration.NodeRegistration.Name = nodeName
|
||||
}
|
||||
|
||||
// SetCertSANs sets the SANs for the certificate.
|
||||
func (k *KubeadmInitYAML) SetCertSANs(certSANs []string) {
|
||||
for _, certSAN := range certSANs {
|
||||
if certSAN == "" {
|
||||
continue
|
||||
}
|
||||
k.ClusterConfiguration.APIServer.CertSANs = append(k.ClusterConfiguration.APIServer.CertSANs, certSAN)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetApiServerAdvertiseAddress(apiServerAdvertiseAddress string) {
|
||||
k.InitConfiguration.LocalAPIEndpoint.AdvertiseAddress = apiServerAdvertiseAddress
|
||||
}
|
||||
|
||||
// SetControlPlaneEndpoint sets the control plane endpoint if controlPlaneEndpoint is not empty.
|
||||
func (k *KubeadmInitYAML) SetControlPlaneEndpoint(controlPlaneEndpoint string) {
|
||||
if controlPlaneEndpoint != "" {
|
||||
k.ClusterConfiguration.ControlPlaneEndpoint = controlPlaneEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetServiceCIDR(serviceCIDR string) {
|
||||
k.ClusterConfiguration.Networking.ServiceSubnet = serviceCIDR
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetPodNetworkCIDR(podNetworkCIDR string) {
|
||||
k.ClusterConfiguration.Networking.PodSubnet = podNetworkCIDR
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetServiceDNSDomain(serviceDNSDomain string) {
|
||||
k.ClusterConfiguration.Networking.DNSDomain = serviceDNSDomain
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetNodeIP(nodeIP string) {
|
||||
if k.InitConfiguration.NodeRegistration.KubeletExtraArgs == nil {
|
||||
k.InitConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{"node-ip": nodeIP}
|
||||
} else {
|
||||
k.InitConfiguration.NodeRegistration.KubeletExtraArgs["node-ip"] = nodeIP
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) SetProviderID(providerID string) {
|
||||
if k.InitConfiguration.NodeRegistration.KubeletExtraArgs == nil {
|
||||
k.InitConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{"provider-id": providerID}
|
||||
} else {
|
||||
k.InitConfiguration.NodeRegistration.KubeletExtraArgs["provider-id"] = providerID
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) Marshal() ([]byte, error) {
|
||||
return resources.MarshalK8SResources(k)
|
||||
}
|
||||
|
||||
func (k *KubeadmInitYAML) Unmarshal(yamlData []byte) (KubeadmInitYAML, error) {
|
||||
var tmp KubeadmInitYAML
|
||||
return tmp, resources.UnmarshalK8SResources(yamlData, &tmp)
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestInitConfiguration(t *testing.T) {
|
||||
coreOSConfig := CoreOSConfiguration{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
config KubeadmInitYAML
|
||||
}{
|
||||
"CoreOS init config can be created": {
|
||||
config: coreOSConfig.InitConfiguration(true),
|
||||
},
|
||||
"CoreOS init config with all fields can be created": {
|
||||
config: func() KubeadmInitYAML {
|
||||
c := coreOSConfig.InitConfiguration(true)
|
||||
c.SetApiServerAdvertiseAddress("192.0.2.0")
|
||||
c.SetNodeIP("192.0.2.0")
|
||||
c.SetNodeName("node")
|
||||
c.SetPodNetworkCIDR("10.244.0.0/16")
|
||||
c.SetServiceCIDR("10.245.0.0/24")
|
||||
c.SetProviderID("somecloudprovider://instance-id")
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
config, err := tc.config.Marshal()
|
||||
require.NoError(err)
|
||||
tmp, err := tc.config.Unmarshal(config)
|
||||
require.NoError(err)
|
||||
// test on correct mashalling and unmarshalling
|
||||
assert.Equal(tc.config.ClusterConfiguration, tmp.ClusterConfiguration)
|
||||
assert.Equal(tc.config.InitConfiguration, tmp.InitConfiguration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinConfiguration(t *testing.T) {
|
||||
coreOSConfig := CoreOSConfiguration{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
config KubeadmJoinYAML
|
||||
}{
|
||||
"CoreOS join config can be created": {
|
||||
config: coreOSConfig.JoinConfiguration(true),
|
||||
},
|
||||
"CoreOS join config with all fields can be created": {
|
||||
config: func() KubeadmJoinYAML {
|
||||
c := coreOSConfig.JoinConfiguration(true)
|
||||
c.SetApiServerEndpoint("192.0.2.0:6443")
|
||||
c.SetNodeIP("192.0.2.0")
|
||||
c.SetNodeName("node")
|
||||
c.SetToken("token")
|
||||
c.AppendDiscoveryTokenCaCertHash("discovery-token-ca-cert-hash")
|
||||
c.SetProviderID("somecloudprovider://instance-id")
|
||||
c.SetControlPlane("192.0.2.0", "11111111111111111111111111111111111")
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
config, err := tc.config.Marshal()
|
||||
require.NoError(err)
|
||||
tmp, err := tc.config.Unmarshal(config)
|
||||
require.NoError(err)
|
||||
// test on correct mashalling and unmarshalling
|
||||
assert.Equal(tc.config.JoinConfiguration, tmp.JoinConfiguration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
11
bootstrapper/internal/kubernetes/k8sapi/kubectl/generator.go
Normal file
11
bootstrapper/internal/kubernetes/k8sapi/kubectl/generator.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package kubectl
|
||||
|
||||
import "github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/kubectl/client"
|
||||
|
||||
// generator implements clientGenerator interface.
|
||||
type generator struct{}
|
||||
|
||||
// NewClients generates a new client implementing the Client interface.
|
||||
func (generator) NewClient(kubeconfig []byte) (Client, error) {
|
||||
return client.New(kubeconfig)
|
||||
}
|
||||
68
bootstrapper/internal/kubernetes/k8sapi/kubectl/kubectl.go
Normal file
68
bootstrapper/internal/kubernetes/k8sapi/kubectl/kubectl.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package kubectl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// ErrKubeconfigNotSet is the error value returned by Kubectl.Apply when SetKubeconfig was not called first.
|
||||
var ErrKubeconfigNotSet = errors.New("kubeconfig not set")
|
||||
|
||||
// Client wraps marshable k8s resources into resource.Info fields and applies them in a cluster.
|
||||
type Client interface {
|
||||
// ApplyOneObject applies a k8s resource similar to kubectl apply.
|
||||
ApplyOneObject(info *resource.Info, forceConflicts bool) error
|
||||
// GetObjects converts resources into prepared info fields for use in ApplyOneObject.
|
||||
GetObjects(resources resources.Marshaler) ([]*resource.Info, error)
|
||||
}
|
||||
|
||||
// clientGenerator can generate new clients from a kubeconfig.
|
||||
type clientGenerator interface {
|
||||
NewClient(kubeconfig []byte) (Client, error)
|
||||
}
|
||||
|
||||
// Kubectl implements kubernetes.Apply interface and acts like the Kubernetes "kubectl" tool.
|
||||
type Kubectl struct {
|
||||
clientGenerator
|
||||
kubeconfig []byte
|
||||
}
|
||||
|
||||
// New creates a new kubectl using the real clientGenerator.
|
||||
func New() *Kubectl {
|
||||
return &Kubectl{
|
||||
clientGenerator: &generator{},
|
||||
}
|
||||
}
|
||||
|
||||
// Apply will apply the given resources using server-side-apply.
|
||||
func (k *Kubectl) Apply(resources resources.Marshaler, forceConflicts bool) error {
|
||||
if k.kubeconfig == nil {
|
||||
return ErrKubeconfigNotSet
|
||||
}
|
||||
client, err := k.clientGenerator.NewClient(k.kubeconfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// convert marshaler object into []*resource.info
|
||||
infos, err := client.GetObjects(resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each object, one by one
|
||||
for i, resource := range infos {
|
||||
if err := client.ApplyOneObject(resource, forceConflicts); err != nil {
|
||||
return fmt.Errorf("kubectl apply of object %v/%v: %w", i, len(infos), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetKubeconfig will store the kubeconfig to generate Clients using the clientGenerator later.
|
||||
func (k *Kubectl) SetKubeconfig(kubeconfig []byte) {
|
||||
k.kubeconfig = kubeconfig
|
||||
}
|
||||
113
bootstrapper/internal/kubernetes/k8sapi/kubectl/kubectl_test.go
Normal file
113
bootstrapper/internal/kubernetes/k8sapi/kubectl/kubectl_test.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package kubectl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
type stubClient struct {
|
||||
applyOneObjectErr error
|
||||
getObjectsInfos []*resource.Info
|
||||
getObjectsErr error
|
||||
}
|
||||
|
||||
func (s *stubClient) ApplyOneObject(info *resource.Info, forceConflicts bool) error {
|
||||
return s.applyOneObjectErr
|
||||
}
|
||||
|
||||
func (s *stubClient) GetObjects(resources resources.Marshaler) ([]*resource.Info, error) {
|
||||
return s.getObjectsInfos, s.getObjectsErr
|
||||
}
|
||||
|
||||
type stubClientGenerator struct {
|
||||
applyOneObjectErr error
|
||||
getObjectsInfos []*resource.Info
|
||||
getObjectsErr error
|
||||
newClientErr error
|
||||
}
|
||||
|
||||
func (s *stubClientGenerator) NewClient(kubeconfig []byte) (Client, error) {
|
||||
return &stubClient{
|
||||
s.applyOneObjectErr,
|
||||
s.getObjectsInfos,
|
||||
s.getObjectsErr,
|
||||
}, s.newClientErr
|
||||
}
|
||||
|
||||
type dummyResource struct{}
|
||||
|
||||
func (*dummyResource) Marshal() ([]byte, error) {
|
||||
panic("dummy")
|
||||
}
|
||||
|
||||
func TestApplyWorks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
kube := Kubectl{
|
||||
clientGenerator: &stubClientGenerator{
|
||||
getObjectsInfos: []*resource.Info{
|
||||
{},
|
||||
},
|
||||
},
|
||||
}
|
||||
kube.SetKubeconfig([]byte("someConfig"))
|
||||
|
||||
assert.NoError(kube.Apply(&dummyResource{}, true))
|
||||
}
|
||||
|
||||
func TestKubeconfigUnset(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
kube := Kubectl{}
|
||||
|
||||
assert.ErrorIs(kube.Apply(&dummyResource{}, true), ErrKubeconfigNotSet)
|
||||
}
|
||||
|
||||
func TestClientGeneratorFails(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := errors.New("generator failed")
|
||||
kube := Kubectl{
|
||||
clientGenerator: &stubClientGenerator{
|
||||
newClientErr: err,
|
||||
},
|
||||
}
|
||||
kube.SetKubeconfig([]byte("someConfig"))
|
||||
|
||||
assert.ErrorIs(kube.Apply(&dummyResource{}, true), err)
|
||||
}
|
||||
|
||||
func TestGetObjectsFails(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := errors.New("getObjects failed")
|
||||
kube := Kubectl{
|
||||
clientGenerator: &stubClientGenerator{
|
||||
getObjectsErr: err,
|
||||
},
|
||||
}
|
||||
kube.SetKubeconfig([]byte("someConfig"))
|
||||
|
||||
assert.ErrorIs(kube.Apply(&dummyResource{}, true), err)
|
||||
}
|
||||
|
||||
func TestApplyOneObjectFails(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := errors.New("applyOneObject failed")
|
||||
kube := Kubectl{
|
||||
clientGenerator: &stubClientGenerator{
|
||||
getObjectsInfos: []*resource.Info{
|
||||
{},
|
||||
},
|
||||
applyOneObjectErr: err,
|
||||
},
|
||||
}
|
||||
kube.SetKubeconfig([]byte("someConfig"))
|
||||
|
||||
assert.ErrorIs(kube.Apply(&dummyResource{}, true), err)
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"github.com/edgelesssys/constellation/internal/secrets"
|
||||
"google.golang.org/protobuf/proto"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// accessManagerDeployment holds the configuration for the SSH user creation pods. User/Key definitions are stored in the ConfigMap, and the manager is deployed on each node by the DaemonSet.
|
||||
type accessManagerDeployment struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
ServiceAccount k8s.ServiceAccount
|
||||
Role rbac.Role
|
||||
RoleBinding rbac.RoleBinding
|
||||
DaemonSet apps.DaemonSet
|
||||
ImagePullSecret k8s.Secret
|
||||
}
|
||||
|
||||
// NewAccessManagerDeployment creates a new *accessManagerDeployment which manages the SSH users for the cluster.
|
||||
func NewAccessManagerDeployment(sshUsers map[string]string) *accessManagerDeployment {
|
||||
return &accessManagerDeployment{
|
||||
ServiceAccount: k8s.ServiceAccount{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "constellation-access-manager",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-access-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
AutomountServiceAccountToken: proto.Bool(true),
|
||||
},
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "ssh-users",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: sshUsers,
|
||||
},
|
||||
DaemonSet: apps.DaemonSet{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "constellation-access-manager",
|
||||
Namespace: "kube-system",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "constellation-access-manager",
|
||||
},
|
||||
},
|
||||
Spec: apps.DaemonSetSpec{
|
||||
Selector: &v1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "constellation-access-manager",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "constellation-access-manager",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
ImagePullSecrets: []k8s.LocalObjectReference{
|
||||
{
|
||||
Name: secrets.PullSecretName,
|
||||
},
|
||||
},
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "pause",
|
||||
Image: "gcr.io/google_containers/pause",
|
||||
ImagePullPolicy: k8s.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
InitContainers: []k8s.Container{
|
||||
{
|
||||
Name: "constellation-access-manager",
|
||||
Image: accessManagerImage,
|
||||
VolumeMounts: []k8s.VolumeMount{
|
||||
{
|
||||
Name: "host",
|
||||
MountPath: "/host",
|
||||
},
|
||||
},
|
||||
SecurityContext: &k8s.SecurityContext{
|
||||
Capabilities: &k8s.Capabilities{
|
||||
Add: []k8s.Capability{
|
||||
"SYS_CHROOT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServiceAccountName: "constellation-access-manager",
|
||||
Volumes: []k8s.Volume{
|
||||
{
|
||||
Name: "host",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
HostPath: &k8s.HostPathVolumeSource{
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Role: rbac.Role{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "Role",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "constellation-access-manager",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-access-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"configmaps",
|
||||
},
|
||||
ResourceNames: []string{
|
||||
"ssh-users",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RoleBinding: rbac.RoleBinding{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "RoleBinding",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "constellation-access-manager",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-access-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: "constellation-access-manager",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "constellation-access-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
ImagePullSecret: NewImagePullSecret(),
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal marshals the access-manager deployment as YAML documents.
|
||||
func (c *accessManagerDeployment) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(c)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestAccessManagerMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
// Without data
|
||||
accessManagerDeplNil := NewAccessManagerDeployment(nil)
|
||||
data, err := accessManagerDeplNil.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
var recreated accessManagerDeployment
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(accessManagerDeplNil, &recreated)
|
||||
|
||||
// With data
|
||||
sshUsers := make(map[string]string)
|
||||
sshUsers["test-user"] = "ssh-rsa abcdefg"
|
||||
accessManagerDeplNil = NewAccessManagerDeployment(sshUsers)
|
||||
data, err = accessManagerDeplNil.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(accessManagerDeplNil, &recreated)
|
||||
}
|
||||
245
bootstrapper/internal/kubernetes/k8sapi/resources/activation.go
Normal file
245
bootstrapper/internal/kubernetes/k8sapi/resources/activation.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/secrets"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
type activationDaemonset struct {
|
||||
ClusterRole rbac.ClusterRole
|
||||
ClusterRoleBinding rbac.ClusterRoleBinding
|
||||
ConfigMap k8s.ConfigMap
|
||||
DaemonSet apps.DaemonSet
|
||||
ServiceAccount k8s.ServiceAccount
|
||||
Service k8s.Service
|
||||
}
|
||||
|
||||
// NewActivationDaemonset returns a daemonset for the activation service.
|
||||
func NewActivationDaemonset(csp, measurementsJSON, idJSON string) *activationDaemonset {
|
||||
return &activationDaemonset{
|
||||
ClusterRole: rbac.ClusterRole{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "activation-service",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "activation-service",
|
||||
},
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"get", "list", "create", "update"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"rbac.authorization.k8s.io"},
|
||||
Resources: []string{"roles", "rolebindings"},
|
||||
Verbs: []string{"create", "update"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterRoleBinding: rbac.ClusterRoleBinding{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "activation-service",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "activation-service",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "activation-service",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
DaemonSet: apps.DaemonSet{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "activation-service",
|
||||
Namespace: "kube-system",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "activation-service",
|
||||
"component": "activation-service",
|
||||
"kubernetes.io/cluster-service": "true",
|
||||
},
|
||||
},
|
||||
Spec: apps.DaemonSetSpec{
|
||||
Selector: &meta.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"k8s-app": "activation-service",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "activation-service",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
PriorityClassName: "system-cluster-critical",
|
||||
ServiceAccountName: "activation-service",
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "CriticalAddonsOnly",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Operator: k8s.TolerationOpEqual,
|
||||
Value: "true",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoExecute,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
// Only run on control plane nodes
|
||||
NodeSelector: map[string]string{
|
||||
"node-role.kubernetes.io/master": "",
|
||||
},
|
||||
ImagePullSecrets: []k8s.LocalObjectReference{
|
||||
{
|
||||
Name: secrets.PullSecretName,
|
||||
},
|
||||
},
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "activation-service",
|
||||
Image: activationImage,
|
||||
Ports: []k8s.ContainerPort{
|
||||
{
|
||||
ContainerPort: constants.ActivationServicePort,
|
||||
Name: "tcp",
|
||||
},
|
||||
},
|
||||
SecurityContext: &k8s.SecurityContext{
|
||||
Privileged: func(b bool) *bool { return &b }(true),
|
||||
},
|
||||
Args: []string{
|
||||
fmt.Sprintf("--cloud-provider=%s", csp),
|
||||
fmt.Sprintf("--kms-endpoint=kms.kube-system:%d", constants.KMSPort),
|
||||
},
|
||||
VolumeMounts: []k8s.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
ReadOnly: true,
|
||||
MountPath: constants.ServiceBasePath,
|
||||
},
|
||||
{
|
||||
Name: "kubeadm",
|
||||
ReadOnly: true,
|
||||
MountPath: "/etc/kubernetes",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []k8s.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
ConfigMap: &k8s.ConfigMapVolumeSource{
|
||||
LocalObjectReference: k8s.LocalObjectReference{
|
||||
Name: "activation-config",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kubeadm",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
HostPath: &k8s.HostPathVolumeSource{
|
||||
Path: "/etc/kubernetes",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServiceAccount: k8s.ServiceAccount{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "activation-service",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
Service: k8s.Service{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "activation-service",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: k8s.ServiceSpec{
|
||||
Type: k8s.ServiceTypeNodePort,
|
||||
Ports: []k8s.ServicePort{
|
||||
{
|
||||
Name: "grpc",
|
||||
Protocol: k8s.ProtocolTCP,
|
||||
Port: constants.ActivationServicePort,
|
||||
TargetPort: intstr.IntOrString{IntVal: constants.ActivationServicePort},
|
||||
NodePort: constants.ActivationServiceNodePort,
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"k8s-app": "activation-service",
|
||||
},
|
||||
},
|
||||
},
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "activation-config",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"measurements": measurementsJSON,
|
||||
"id": idJSON,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the daemonset using the Kubernetes resource marshaller.
|
||||
func (a *activationDaemonset) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(a)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewActivationDaemonset(t *testing.T) {
|
||||
deployment := NewActivationDaemonset("csp", "measurementsJSON", "idJSON")
|
||||
deploymentYAML, err := deployment.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var recreated activationDaemonset
|
||||
require.NoError(t, UnmarshalK8SResources(deploymentYAML, &recreated))
|
||||
assert.Equal(t, deployment, &recreated)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
|
||||
)
|
||||
|
||||
// AuditPolicy defines rulesets for what should be logged in the kube-apiserver audit log.
|
||||
// reference: https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/ .
|
||||
type AuditPolicy struct {
|
||||
Policy auditv1.Policy
|
||||
}
|
||||
|
||||
func NewDefaultAuditPolicy() *AuditPolicy {
|
||||
return &AuditPolicy{
|
||||
Policy: auditv1.Policy{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "audit.k8s.io/v1",
|
||||
Kind: "Policy",
|
||||
},
|
||||
Rules: []auditv1.PolicyRule{
|
||||
{
|
||||
Level: auditv1.LevelMetadata,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal marshals the audit policy as a YAML document.
|
||||
func (p *AuditPolicy) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(p)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuditPolicyMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
auditPolicy := NewDefaultAuditPolicy()
|
||||
data, err := auditPolicy.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
var recreated AuditPolicy
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(auditPolicy, &recreated)
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type cloudControllerManagerDeployment struct {
|
||||
ServiceAccount k8s.ServiceAccount
|
||||
ClusterRoleBinding rbac.ClusterRoleBinding
|
||||
DaemonSet apps.DaemonSet
|
||||
}
|
||||
|
||||
// references:
|
||||
// https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/admin/cloud/ccm-example.yaml
|
||||
// https://kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/#cloud-controller-manager
|
||||
|
||||
// NewDefaultCloudControllerManagerDeployment creates a new *cloudControllerManagerDeployment, customized for the CSP.
|
||||
func NewDefaultCloudControllerManagerDeployment(cloudProvider, image, path, podCIDR string, extraArgs []string, extraVolumes []k8s.Volume, extraVolumeMounts []k8s.VolumeMount, env []k8s.EnvVar) *cloudControllerManagerDeployment {
|
||||
command := []string{
|
||||
path,
|
||||
fmt.Sprintf("--cloud-provider=%s", cloudProvider),
|
||||
"--leader-elect=true",
|
||||
fmt.Sprintf("--cluster-cidr=%s", podCIDR),
|
||||
"-v=2",
|
||||
}
|
||||
command = append(command, extraArgs...)
|
||||
volumes := []k8s.Volume{
|
||||
{
|
||||
Name: "etckubernetes",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
HostPath: &k8s.HostPathVolumeSource{Path: "/etc/kubernetes"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "etcssl",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
HostPath: &k8s.HostPathVolumeSource{Path: "/etc/ssl"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "etcpki",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
HostPath: &k8s.HostPathVolumeSource{Path: "/etc/pki"},
|
||||
},
|
||||
},
|
||||
}
|
||||
volumes = append(volumes, extraVolumes...)
|
||||
volumeMounts := []k8s.VolumeMount{
|
||||
{
|
||||
MountPath: "/etc/kubernetes",
|
||||
Name: "etckubernetes",
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
MountPath: "/etc/ssl",
|
||||
Name: "etcssl",
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
MountPath: "/etc/pki",
|
||||
Name: "etcpki",
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
volumeMounts = append(volumeMounts, extraVolumeMounts...)
|
||||
|
||||
return &cloudControllerManagerDeployment{
|
||||
ServiceAccount: k8s.ServiceAccount{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cloud-controller-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
ClusterRoleBinding: rbac.ClusterRoleBinding{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "system:cloud-controller-manager",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "cluster-admin",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "cloud-controller-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
DaemonSet: apps.DaemonSet{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "cloud-controller-manager",
|
||||
},
|
||||
Name: "cloud-controller-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: apps.DaemonSetSpec{
|
||||
Selector: &meta.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"k8s-app": "cloud-controller-manager",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "cloud-controller-manager",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
ServiceAccountName: "cloud-controller-manager",
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "cloud-controller-manager",
|
||||
Image: image,
|
||||
Command: command,
|
||||
VolumeMounts: volumeMounts,
|
||||
Env: env,
|
||||
},
|
||||
},
|
||||
Volumes: volumes,
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "node.cloudprovider.kubernetes.io/uninitialized",
|
||||
Value: "true",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node.kubernetes.io/not-ready",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
NodeSelector: map[string]string{
|
||||
"node-role.kubernetes.io/master": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloudControllerManagerDeployment) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(c)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestCloudControllerMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
cloudControllerManagerDepl := NewDefaultCloudControllerManagerDeployment("dummy-cloudprovider", "some-image:latest", "/dummy_path", "192.0.2.0/24", []string{}, []k8s.Volume{}, []k8s.VolumeMount{}, nil)
|
||||
data, err := cloudControllerManagerDepl.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
var recreated cloudControllerManagerDeployment
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(cloudControllerManagerDepl, &recreated)
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type cloudNodeManagerDeployment struct {
|
||||
ServiceAccount k8s.ServiceAccount
|
||||
ClusterRole rbac.ClusterRole
|
||||
ClusterRoleBinding rbac.ClusterRoleBinding
|
||||
DaemonSet apps.DaemonSet
|
||||
}
|
||||
|
||||
// NewDefaultCloudNodeManagerDeployment creates a new *cloudNodeManagerDeployment, customized for the CSP.
|
||||
func NewDefaultCloudNodeManagerDeployment(image, path string, extraArgs []string) *cloudNodeManagerDeployment {
|
||||
command := []string{
|
||||
path,
|
||||
"--node-name=$(NODE_NAME)",
|
||||
}
|
||||
command = append(command, extraArgs...)
|
||||
return &cloudNodeManagerDeployment{
|
||||
ServiceAccount: k8s.ServiceAccount{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cloud-node-manager",
|
||||
Namespace: "kube-system",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "cloud-node-manager",
|
||||
"kubernetes.io/cluster-service": "true",
|
||||
"addonmanager.kubernetes.io/mode": "Reconcile",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterRole: rbac.ClusterRole{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cloud-node-manager",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "cloud-node-manager",
|
||||
"kubernetes.io/cluster-service": "true",
|
||||
"addonmanager.kubernetes.io/mode": "Reconcile",
|
||||
},
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"nodes"},
|
||||
Verbs: []string{"watch", "list", "get", "update", "patch"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"nodes/status"},
|
||||
Verbs: []string{"patch"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterRoleBinding: rbac.ClusterRoleBinding{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cloud-node-manager",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "cloud-node-manager",
|
||||
"kubernetes.io/cluster-service": "true",
|
||||
"addonmanager.kubernetes.io/mode": "Reconcile",
|
||||
},
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "cloud-node-manager",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "cloud-node-manager",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
DaemonSet: apps.DaemonSet{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "cloud-node-manager",
|
||||
Namespace: "kube-system",
|
||||
Labels: map[string]string{
|
||||
"component": "cloud-node-manager",
|
||||
"kubernetes.io/cluster-service": "true",
|
||||
"addonmanager.kubernetes.io/mode": "Reconcile",
|
||||
},
|
||||
},
|
||||
Spec: apps.DaemonSetSpec{
|
||||
Selector: &meta.LabelSelector{
|
||||
MatchLabels: map[string]string{"k8s-app": "cloud-node-manager"},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{"k8s-app": "cloud-node-manager"},
|
||||
Annotations: map[string]string{"cluster-autoscaler.kubernetes.io/daemonset-pod": "true"},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
PriorityClassName: "system-node-critical",
|
||||
ServiceAccountName: "cloud-node-manager",
|
||||
HostNetwork: true,
|
||||
NodeSelector: map[string]string{"kubernetes.io/os": "linux"},
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "CriticalAddonsOnly",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Operator: k8s.TolerationOpEqual,
|
||||
Value: "true",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoExecute,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "cloud-node-manager",
|
||||
Image: image,
|
||||
ImagePullPolicy: k8s.PullIfNotPresent,
|
||||
Command: command,
|
||||
Env: []k8s.EnvVar{
|
||||
{
|
||||
Name: "NODE_NAME",
|
||||
ValueFrom: &k8s.EnvVarSource{
|
||||
FieldRef: &k8s.ObjectFieldSelector{
|
||||
FieldPath: "spec.nodeName",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Resources: k8s.ResourceRequirements{
|
||||
Requests: k8s.ResourceList{
|
||||
k8s.ResourceCPU: resource.MustParse("50m"),
|
||||
k8s.ResourceMemory: resource.MustParse("50Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal marshals the cloud-node-manager deployment as YAML documents.
|
||||
func (c *cloudNodeManagerDeployment) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(c)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCloudNodeManagerMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
cloudNodeManagerDepl := NewDefaultCloudNodeManagerDeployment("image", "path", []string{})
|
||||
data, err := cloudNodeManagerDepl.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
var recreated cloudNodeManagerDeployment
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(cloudNodeManagerDepl, &recreated)
|
||||
}
|
||||
|
|
@ -0,0 +1,505 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1"
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
type autoscalerDeployment struct {
|
||||
PodDisruptionBudget policy.PodDisruptionBudget
|
||||
ServiceAccount k8s.ServiceAccount
|
||||
ClusterRole rbac.ClusterRole
|
||||
ClusterRoleBinding rbac.ClusterRoleBinding
|
||||
Role rbac.Role
|
||||
RoleBinding rbac.RoleBinding
|
||||
Service k8s.Service
|
||||
Deployment apps.Deployment
|
||||
}
|
||||
|
||||
// NewDefaultAutoscalerDeployment creates a new *autoscalerDeployment, customized for the CSP.
|
||||
func NewDefaultAutoscalerDeployment(extraVolumes []k8s.Volume, extraVolumeMounts []k8s.VolumeMount, env []k8s.EnvVar) *autoscalerDeployment {
|
||||
return &autoscalerDeployment{
|
||||
PodDisruptionBudget: policy.PodDisruptionBudget{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "policy/v1",
|
||||
Kind: "PodDisruptionBudget",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: policy.PodDisruptionBudgetSpec{
|
||||
Selector: &v1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
},
|
||||
},
|
||||
MaxUnavailable: &intstr.IntOrString{
|
||||
Type: intstr.Int,
|
||||
IntVal: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
ServiceAccount: k8s.ServiceAccount{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
AutomountServiceAccountToken: proto.Bool(true),
|
||||
},
|
||||
ClusterRole: rbac.ClusterRole{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"events",
|
||||
"endpoints",
|
||||
},
|
||||
Verbs: []string{
|
||||
"create",
|
||||
"patch",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"pods/eviction",
|
||||
},
|
||||
Verbs: []string{
|
||||
"create",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"pods/status",
|
||||
},
|
||||
Verbs: []string{
|
||||
"update",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"endpoints",
|
||||
},
|
||||
ResourceNames: []string{
|
||||
"cluster-autoscaler",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"update",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"nodes",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
"update",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"namespaces",
|
||||
"pods",
|
||||
"services",
|
||||
"replicationcontrollers",
|
||||
"persistentvolumeclaims",
|
||||
"persistentvolumes",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"batch",
|
||||
},
|
||||
Resources: []string{
|
||||
"jobs",
|
||||
"cronjobs",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"batch",
|
||||
"extensions",
|
||||
},
|
||||
Resources: []string{
|
||||
"jobs",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"list",
|
||||
"patch",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"extensions",
|
||||
},
|
||||
Resources: []string{
|
||||
"replicasets",
|
||||
"daemonsets",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"policy",
|
||||
},
|
||||
Resources: []string{
|
||||
"poddisruptionbudgets",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"apps",
|
||||
},
|
||||
Resources: []string{
|
||||
"daemonsets",
|
||||
"replicasets",
|
||||
"statefulsets",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"storage.k8s.io",
|
||||
},
|
||||
Resources: []string{
|
||||
"storageclasses",
|
||||
"csinodes",
|
||||
"csidrivers",
|
||||
"csistoragecapacities",
|
||||
},
|
||||
Verbs: []string{
|
||||
"watch",
|
||||
"list",
|
||||
"get",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"configmaps",
|
||||
},
|
||||
Verbs: []string{
|
||||
"list",
|
||||
"watch",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"coordination.k8s.io",
|
||||
},
|
||||
Resources: []string{
|
||||
"leases",
|
||||
},
|
||||
Verbs: []string{
|
||||
"create",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{
|
||||
"coordination.k8s.io",
|
||||
},
|
||||
ResourceNames: []string{
|
||||
"cluster-autoscaler",
|
||||
},
|
||||
Resources: []string{
|
||||
"leases",
|
||||
},
|
||||
Verbs: []string{
|
||||
"get",
|
||||
"update",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterRoleBinding: rbac.ClusterRoleBinding{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
Role: rbac.Role{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "Role",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"configmaps",
|
||||
},
|
||||
Verbs: []string{
|
||||
"create",
|
||||
},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{
|
||||
"configmaps",
|
||||
},
|
||||
ResourceNames: []string{
|
||||
"cluster-autoscaler-status",
|
||||
},
|
||||
Verbs: []string{
|
||||
"delete",
|
||||
"get",
|
||||
"update",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RoleBinding: rbac.RoleBinding{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "RoleBinding",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
Service: k8s.Service{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: k8s.ServiceSpec{
|
||||
Ports: []k8s.ServicePort{
|
||||
{
|
||||
Port: 8085,
|
||||
Protocol: k8s.ProtocolTCP,
|
||||
TargetPort: intstr.FromInt(8085),
|
||||
Name: "http",
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
},
|
||||
Type: k8s.ServiceTypeClusterIP,
|
||||
},
|
||||
},
|
||||
Deployment: apps.Deployment{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
"app.kubernetes.io/managed-by": "Constellation",
|
||||
},
|
||||
Name: "constellation-cluster-autoscaler",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: proto.Int32(1),
|
||||
Selector: &v1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "constellation",
|
||||
"app.kubernetes.io/name": "cluster-autoscaler",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
PriorityClassName: "system-cluster-critical",
|
||||
DNSPolicy: k8s.DNSClusterFirst,
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "cluster-autoscaler",
|
||||
Image: clusterAutoscalerImage,
|
||||
ImagePullPolicy: k8s.PullIfNotPresent,
|
||||
LivenessProbe: &k8s.Probe{
|
||||
ProbeHandler: k8s.ProbeHandler{
|
||||
HTTPGet: &k8s.HTTPGetAction{
|
||||
Path: "/health-check",
|
||||
Port: intstr.FromInt(8085),
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: []k8s.ContainerPort{
|
||||
{
|
||||
ContainerPort: 8085,
|
||||
},
|
||||
},
|
||||
VolumeMounts: extraVolumeMounts,
|
||||
Env: env,
|
||||
},
|
||||
},
|
||||
Volumes: extraVolumes,
|
||||
ServiceAccountName: "constellation-cluster-autoscaler",
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node.cloudprovider.kubernetes.io/uninitialized",
|
||||
Operator: k8s.TolerationOpEqual,
|
||||
Value: "true",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
NodeSelector: map[string]string{
|
||||
"node-role.kubernetes.io/control-plane": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *autoscalerDeployment) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(a)
|
||||
}
|
||||
|
||||
func (a *autoscalerDeployment) SetAutoscalerCommand(cloudprovider string, autoscalingNodeGroups []string) {
|
||||
command := []string{
|
||||
"./cluster-autoscaler",
|
||||
"--cloud-provider",
|
||||
cloudprovider,
|
||||
"--logtostderr=true",
|
||||
"--stderrthreshold=info",
|
||||
"--v=2",
|
||||
"--namespace=kube-system",
|
||||
}
|
||||
for _, autoscalingNodeGroup := range autoscalingNodeGroups {
|
||||
command = append(command, "--nodes", autoscalingNodeGroup)
|
||||
}
|
||||
|
||||
a.Deployment.Spec.Template.Spec.Containers[0].Command = command
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutoscalerDeploymentMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
autoscalerDepl := NewDefaultAutoscalerDeployment(nil, nil, nil)
|
||||
|
||||
data, err := autoscalerDepl.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
t.Log(string(data))
|
||||
|
||||
var recreated autoscalerDeployment
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(autoscalerDepl, &recreated)
|
||||
}
|
||||
|
||||
func TestAutoscalerDeploymentWithCommandMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
autoscalerDepl := NewDefaultAutoscalerDeployment(nil, nil, nil)
|
||||
autoscalerDepl.SetAutoscalerCommand("someProvider", []string{"group1", "group2"})
|
||||
|
||||
data, err := autoscalerDepl.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
t.Log(string(data))
|
||||
|
||||
var recreated autoscalerDeployment
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(autoscalerDepl, &recreated)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
k8s "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// ConfigMaps represent a list of k8s ConfigMap.
|
||||
type ConfigMaps []*k8s.ConfigMap
|
||||
|
||||
// Marshal marshals config maps into multiple YAML documents.
|
||||
func (s ConfigMaps) Marshal() ([]byte, error) {
|
||||
objects := make([]runtime.Object, len(s))
|
||||
for i := range s {
|
||||
objects[i] = s[i]
|
||||
}
|
||||
return MarshalK8SResourcesList(objects)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestConfigMaps(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
configMaps := ConfigMaps{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{"key": "value1"},
|
||||
},
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{"key": "value2"},
|
||||
},
|
||||
}
|
||||
data, err := configMaps.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(`apiVersion: v1
|
||||
data:
|
||||
key: value1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: value2
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`, string(data))
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/secrets"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// NewImagePullSecret creates a new k8s.Secret from the config for authenticating when pulling images.
|
||||
func NewImagePullSecret() k8s.Secret {
|
||||
base64EncodedSecret := base64.StdEncoding.EncodeToString(
|
||||
[]byte(fmt.Sprintf("%s:%s", secrets.PullSecretUser, secrets.PullSecretToken)),
|
||||
)
|
||||
|
||||
pullSecretDockerCfgJson := fmt.Sprintf(`{"auths":{"ghcr.io":{"auth":"%s"}}}`, base64EncodedSecret)
|
||||
|
||||
return k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: secrets.PullSecretName,
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
StringData: map[string]string{".dockerconfigjson": pullSecretDockerCfgJson},
|
||||
Type: "kubernetes.io/dockerconfigjson",
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestImagePullSecret(t *testing.T) {
|
||||
imgPullSec := NewImagePullSecret()
|
||||
_, err := imgPullSec.Marshal()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
12
bootstrapper/internal/kubernetes/k8sapi/resources/images.go
Normal file
12
bootstrapper/internal/kubernetes/k8sapi/resources/images.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package resources
|
||||
|
||||
const (
|
||||
// Constellation images.
|
||||
activationImage = "ghcr.io/edgelesssys/constellation/activation-service:v1.2"
|
||||
accessManagerImage = "ghcr.io/edgelesssys/constellation/access-manager:v1.2"
|
||||
kmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v1.2"
|
||||
verificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v1.2"
|
||||
|
||||
// external images.
|
||||
clusterAutoscalerImage = "k8s.gcr.io/autoscaling/cluster-autoscaler:v1.23.0"
|
||||
)
|
||||
268
bootstrapper/internal/kubernetes/k8sapi/resources/kms.go
Normal file
268
bootstrapper/internal/kubernetes/k8sapi/resources/kms.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/secrets"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
rbac "k8s.io/api/rbac/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
type kmsDeployment struct {
|
||||
ServiceAccount k8s.ServiceAccount
|
||||
ServiceInternal k8s.Service
|
||||
ServiceExternal k8s.Service
|
||||
ClusterRole rbac.ClusterRole
|
||||
ClusterRoleBinding rbac.ClusterRoleBinding
|
||||
Deployment apps.Deployment
|
||||
MasterSecret k8s.Secret
|
||||
ImagePullSecret k8s.Secret
|
||||
}
|
||||
|
||||
// NewKMSDeployment creates a new *kmsDeployment to use as the key management system inside Constellation.
|
||||
func NewKMSDeployment(csp string, masterSecret []byte) *kmsDeployment {
|
||||
return &kmsDeployment{
|
||||
ServiceAccount: k8s.ServiceAccount{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "kms",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
ServiceInternal: k8s.Service{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "kms",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: k8s.ServiceSpec{
|
||||
Type: k8s.ServiceTypeClusterIP,
|
||||
Ports: []k8s.ServicePort{
|
||||
{
|
||||
Name: "grpc",
|
||||
Protocol: k8s.ProtocolTCP,
|
||||
Port: constants.KMSPort,
|
||||
TargetPort: intstr.FromInt(constants.KMSPort),
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"k8s-app": "kms",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServiceExternal: k8s.Service{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "kms-external",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: k8s.ServiceSpec{
|
||||
Type: k8s.ServiceTypeNodePort,
|
||||
Ports: []k8s.ServicePort{
|
||||
{
|
||||
Name: "atls",
|
||||
Protocol: k8s.ProtocolTCP,
|
||||
Port: constants.KMSATLSPort,
|
||||
TargetPort: intstr.FromInt(constants.KMSATLSPort),
|
||||
NodePort: constants.KMSNodePort,
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"k8s-app": "kms",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterRole: rbac.ClusterRole{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "kms",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "kms",
|
||||
},
|
||||
},
|
||||
Rules: []rbac.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterRoleBinding: rbac.ClusterRoleBinding{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "kms",
|
||||
},
|
||||
RoleRef: rbac.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "kms",
|
||||
},
|
||||
Subjects: []rbac.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "kms",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
Deployment: apps.Deployment{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "kms",
|
||||
},
|
||||
Name: "kms",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Selector: &meta.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"k8s-app": "kms",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "kms",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
PriorityClassName: "system-cluster-critical",
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "CriticalAddonsOnly",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Operator: k8s.TolerationOpEqual,
|
||||
Value: "true",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoExecute,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
// Only run on control plane nodes
|
||||
NodeSelector: map[string]string{
|
||||
"node-role.kubernetes.io/master": "",
|
||||
},
|
||||
ImagePullSecrets: []k8s.LocalObjectReference{
|
||||
{
|
||||
Name: secrets.PullSecretName,
|
||||
},
|
||||
},
|
||||
Volumes: []k8s.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
Projected: &k8s.ProjectedVolumeSource{
|
||||
Sources: []k8s.VolumeProjection{
|
||||
{
|
||||
ConfigMap: &k8s.ConfigMapProjection{
|
||||
LocalObjectReference: k8s.LocalObjectReference{
|
||||
Name: "activation-config",
|
||||
},
|
||||
Items: []k8s.KeyToPath{
|
||||
{
|
||||
Key: constants.MeasurementsFilename,
|
||||
Path: constants.MeasurementsFilename,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Secret: &k8s.SecretProjection{
|
||||
LocalObjectReference: k8s.LocalObjectReference{
|
||||
Name: constants.ConstellationMasterSecretStoreName,
|
||||
},
|
||||
Items: []k8s.KeyToPath{
|
||||
{
|
||||
Key: constants.ConstellationMasterSecretKey,
|
||||
Path: constants.MasterSecretFilename,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServiceAccountName: "kms",
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "kms",
|
||||
Image: kmsImage,
|
||||
Args: []string{
|
||||
fmt.Sprintf("--atls-port=%d", constants.KMSATLSPort),
|
||||
fmt.Sprintf("--port=%d", constants.KMSPort),
|
||||
fmt.Sprintf("--cloud-provider=%s", csp),
|
||||
},
|
||||
VolumeMounts: []k8s.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
ReadOnly: true,
|
||||
MountPath: constants.ServiceBasePath,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MasterSecret: k8s.Secret{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: constants.ConstellationMasterSecretStoreName,
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
constants.ConstellationMasterSecretKey: masterSecret,
|
||||
},
|
||||
Type: "Opaque",
|
||||
},
|
||||
ImagePullSecret: NewImagePullSecret(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *kmsDeployment) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(c)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKMSMarshalUnmarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
testMS := []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8}
|
||||
kmsDepl := NewKMSDeployment("test", testMS)
|
||||
data, err := kmsDepl.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
var recreated kmsDeployment
|
||||
require.NoError(UnmarshalK8SResources(data, &recreated))
|
||||
assert.Equal(kmsDepl, &recreated)
|
||||
}
|
||||
149
bootstrapper/internal/kubernetes/k8sapi/resources/marshal.go
Normal file
149
bootstrapper/internal/kubernetes/k8sapi/resources/marshal.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
// Marshaler is used by all k8s resources that can be marshaled to YAML.
|
||||
type Marshaler interface {
|
||||
Marshal() ([]byte, error)
|
||||
}
|
||||
|
||||
// MarshalK8SResources marshals every field of a struct into a k8s resource YAML.
|
||||
func MarshalK8SResources(resources any) ([]byte, error) {
|
||||
if resources == nil {
|
||||
return nil, errors.New("marshal on nil called")
|
||||
}
|
||||
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil)
|
||||
var buf bytes.Buffer
|
||||
|
||||
// reflect over struct containing fields that are k8s resources
|
||||
value := reflect.ValueOf(resources)
|
||||
if value.Kind() != reflect.Ptr && value.Kind() != reflect.Interface {
|
||||
return nil, errors.New("marshal on non-pointer called")
|
||||
}
|
||||
elem := value.Elem()
|
||||
if elem.Kind() == reflect.Struct {
|
||||
// iterate over all struct fields
|
||||
for i := 0; i < elem.NumField(); i++ {
|
||||
field := elem.Field(i)
|
||||
var inter any
|
||||
// check if value can be converted to interface
|
||||
if field.CanInterface() {
|
||||
inter = field.Addr().Interface()
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// convert field interface to runtime.Object
|
||||
obj, ok := inter.(runtime.Object)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
// separate YAML documents
|
||||
buf.Write([]byte("---\n"))
|
||||
}
|
||||
// serialize k8s resource
|
||||
if err := serializer.Encode(obj, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalK8SResources takes YAML and converts it into a k8s resources struct.
|
||||
func UnmarshalK8SResources(data []byte, into any) error {
|
||||
if into == nil {
|
||||
return errors.New("unmarshal on nil called")
|
||||
}
|
||||
// reflect over struct containing fields that are k8s resources
|
||||
value := reflect.ValueOf(into).Elem()
|
||||
if value.Kind() != reflect.Struct {
|
||||
return errors.New("can only reflect over struct")
|
||||
}
|
||||
|
||||
decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDecoder()
|
||||
documents, err := splitYAML(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("splitting deployment YAML into multiple documents: %w", err)
|
||||
}
|
||||
if len(documents) != value.NumField() {
|
||||
return fmt.Errorf("expected %v YAML documents, got %v", value.NumField(), len(documents))
|
||||
}
|
||||
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := value.Field(i)
|
||||
var inter any
|
||||
// check if value can be converted to interface
|
||||
if !field.CanInterface() {
|
||||
return fmt.Errorf("cannot use struct field %v as interface", i)
|
||||
}
|
||||
inter = field.Addr().Interface()
|
||||
// convert field interface to runtime.Object
|
||||
obj, ok := inter.(runtime.Object)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot convert struct field %v as k8s runtime object", i)
|
||||
}
|
||||
|
||||
// decode YAML document into struct field
|
||||
if err := runtime.DecodeInto(decoder, documents[i], obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalK8SResourcesList marshals every element of a slice into a k8s resource YAML.
|
||||
func MarshalK8SResourcesList(resources []runtime.Object) ([]byte, error) {
|
||||
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil)
|
||||
var buf bytes.Buffer
|
||||
|
||||
for i, obj := range resources {
|
||||
if i > 0 {
|
||||
// separate YAML documents
|
||||
buf.Write([]byte("---\n"))
|
||||
}
|
||||
// serialize k8s resource
|
||||
if err := serializer.Encode(obj, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// splitYAML splits a YAML multidoc into a slice of multiple YAML docs.
|
||||
func splitYAML(resources []byte) ([][]byte, error) {
|
||||
dec := yaml.NewDecoder(bytes.NewReader(resources))
|
||||
var res [][]byte
|
||||
for {
|
||||
var value any
|
||||
err := dec.Decode(&value)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueBytes, err := yaml.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, valueBytes)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestMarshalK8SResources(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
resources any
|
||||
wantErr bool
|
||||
wantYAML string
|
||||
}{
|
||||
"ConfigMap as only field can be marshaled": {
|
||||
resources: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Multiple fields are correctly encoded": {
|
||||
resources: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
Secret k8s.Secret
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Secret: k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Non-pointer is detected": {
|
||||
resources: "non-pointer",
|
||||
wantErr: true,
|
||||
},
|
||||
"Nil resource pointer is detected": {
|
||||
resources: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
"Non-pointer field is ignored": {
|
||||
resources: &struct{ String string }{String: "somestring"},
|
||||
},
|
||||
"nil field is ignored": {
|
||||
resources: &struct {
|
||||
ConfigMap *k8s.ConfigMap
|
||||
}{
|
||||
ConfigMap: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
yaml, err := MarshalK8SResources(tc.resources)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.wantYAML, string(yaml))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalK8SResources(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
data string
|
||||
into any
|
||||
wantObj any
|
||||
wantErr bool
|
||||
}{
|
||||
"ConfigMap as only field can be unmarshaled": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{},
|
||||
wantObj: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Multiple fields are correctly unmarshaled": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
Secret k8s.Secret
|
||||
}{},
|
||||
wantObj: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
Secret k8s.Secret
|
||||
}{
|
||||
ConfigMap: k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Secret: k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Mismatching amount of fields is detected": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
"Non-struct pointer is detected": {
|
||||
into: proto.String("test"),
|
||||
wantErr: true,
|
||||
},
|
||||
"Nil into is detected": {
|
||||
into: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
"Invalid yaml is detected": {
|
||||
data: `duplicateKey: value
|
||||
duplicateKey: value`,
|
||||
into: &struct {
|
||||
ConfigMap k8s.ConfigMap
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
"Struct field cannot interface with runtime.Object": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
String string
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
"Struct field mismatch": {
|
||||
data: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
into: &struct {
|
||||
Secret k8s.Secret
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
err := UnmarshalK8SResources([]byte(tc.data), tc.into)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.wantObj, tc.into)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalK8SResourcesList(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
resources []runtime.Object
|
||||
wantErr bool
|
||||
wantYAML string
|
||||
}{
|
||||
"ConfigMap as only element be marshaled": {
|
||||
resources: []runtime.Object{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Multiple fields are correctly encoded": {
|
||||
resources: []runtime.Object{
|
||||
&k8s.ConfigMap{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantYAML: `apiVersion: v1
|
||||
data:
|
||||
key: value
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`,
|
||||
},
|
||||
"Nil resource pointer is encodes": {
|
||||
resources: []runtime.Object{nil},
|
||||
wantYAML: "null\n",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
yaml, err := MarshalK8SResourcesList(tc.resources)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(tc.wantYAML, string(yaml))
|
||||
})
|
||||
}
|
||||
}
|
||||
18
bootstrapper/internal/kubernetes/k8sapi/resources/secrets.go
Normal file
18
bootstrapper/internal/kubernetes/k8sapi/resources/secrets.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
k8s "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// ConfigMaps represent a list of k8s Secret.
|
||||
type Secrets []*k8s.Secret
|
||||
|
||||
// Marshal marshals secrets into multiple YAML documents.
|
||||
func (s Secrets) Marshal() ([]byte, error) {
|
||||
objects := make([]runtime.Object, len(s))
|
||||
for i := range s {
|
||||
objects[i] = s[i]
|
||||
}
|
||||
return MarshalK8SResourcesList(objects)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
secrets := Secrets{
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value1")},
|
||||
},
|
||||
&k8s.Secret{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value2")},
|
||||
},
|
||||
}
|
||||
data, err := secrets.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
assert.Equal(`apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWUx
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
---
|
||||
apiVersion: v1
|
||||
data:
|
||||
key: dmFsdWUy
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
`, string(data))
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/secrets"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
k8s "k8s.io/api/core/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
type verificationDaemonset struct {
|
||||
DaemonSet apps.DaemonSet
|
||||
Service k8s.Service
|
||||
}
|
||||
|
||||
func NewVerificationDaemonSet(csp string) *verificationDaemonset {
|
||||
return &verificationDaemonset{
|
||||
DaemonSet: apps.DaemonSet{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "verification-service",
|
||||
Namespace: "kube-system",
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "verification-service",
|
||||
"component": "verification-service",
|
||||
},
|
||||
},
|
||||
Spec: apps.DaemonSetSpec{
|
||||
Selector: &meta.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"k8s-app": "verification-service",
|
||||
},
|
||||
},
|
||||
Template: k8s.PodTemplateSpec{
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"k8s-app": "verification-service",
|
||||
},
|
||||
},
|
||||
Spec: k8s.PodSpec{
|
||||
Tolerations: []k8s.Toleration{
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Operator: k8s.TolerationOpEqual,
|
||||
Value: "true",
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: "node-role.kubernetes.io/control-plane",
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoExecute,
|
||||
},
|
||||
{
|
||||
Operator: k8s.TolerationOpExists,
|
||||
Effect: k8s.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
ImagePullSecrets: []k8s.LocalObjectReference{
|
||||
{
|
||||
Name: secrets.PullSecretName,
|
||||
},
|
||||
},
|
||||
Containers: []k8s.Container{
|
||||
{
|
||||
Name: "verification-service",
|
||||
Image: verificationImage,
|
||||
Ports: []k8s.ContainerPort{
|
||||
{
|
||||
Name: "http",
|
||||
ContainerPort: constants.VerifyServicePortHTTP,
|
||||
},
|
||||
{
|
||||
Name: "grpc",
|
||||
ContainerPort: constants.VerifyServicePortGRPC,
|
||||
},
|
||||
},
|
||||
SecurityContext: &k8s.SecurityContext{
|
||||
Privileged: func(b bool) *bool { return &b }(true),
|
||||
},
|
||||
Args: []string{
|
||||
fmt.Sprintf("--cloud-provider=%s", csp),
|
||||
},
|
||||
VolumeMounts: []k8s.VolumeMount{
|
||||
{
|
||||
Name: "event-log",
|
||||
ReadOnly: true,
|
||||
MountPath: "/sys/kernel/security/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []k8s.Volume{
|
||||
{
|
||||
Name: "event-log",
|
||||
VolumeSource: k8s.VolumeSource{
|
||||
HostPath: &k8s.HostPathVolumeSource{
|
||||
Path: "/sys/kernel/security/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Service: k8s.Service{
|
||||
TypeMeta: meta.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
},
|
||||
ObjectMeta: meta.ObjectMeta{
|
||||
Name: "verification-service",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
Spec: k8s.ServiceSpec{
|
||||
Type: k8s.ServiceTypeNodePort,
|
||||
Ports: []k8s.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Protocol: k8s.ProtocolTCP,
|
||||
Port: constants.VerifyServicePortHTTP,
|
||||
TargetPort: intstr.FromInt(constants.VerifyServicePortHTTP),
|
||||
NodePort: constants.VerifyServiceNodePortHTTP,
|
||||
},
|
||||
{
|
||||
Name: "grpc",
|
||||
Protocol: k8s.ProtocolTCP,
|
||||
Port: constants.VerifyServicePortGRPC,
|
||||
TargetPort: intstr.FromInt(constants.VerifyServicePortGRPC),
|
||||
NodePort: constants.VerifyServiceNodePortGRPC,
|
||||
},
|
||||
},
|
||||
Selector: map[string]string{
|
||||
"k8s-app": "verification-service",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (v *verificationDaemonset) Marshal() ([]byte, error) {
|
||||
return MarshalK8SResources(v)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewVerificationDaemonset(t *testing.T) {
|
||||
deployment := NewVerificationDaemonSet("csp")
|
||||
deploymentYAML, err := deployment.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var recreated verificationDaemonset
|
||||
require.NoError(t, UnmarshalK8SResources(deploymentYAML, &recreated))
|
||||
assert.Equal(t, deployment, &recreated)
|
||||
}
|
||||
68
bootstrapper/internal/kubernetes/k8sapi/systemd.go
Normal file
68
bootstrapper/internal/kubernetes/k8sapi/systemd.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/dbus"
|
||||
)
|
||||
|
||||
func restartSystemdUnit(ctx context.Context, unit string) error {
|
||||
conn, err := dbus.NewSystemdConnectionContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("establishing systemd connection: %w", err)
|
||||
}
|
||||
|
||||
restartChan := make(chan string)
|
||||
if _, err := conn.RestartUnitContext(ctx, unit, "replace", restartChan); err != nil {
|
||||
return fmt.Errorf("restarting systemd unit %q: %w", unit, err)
|
||||
}
|
||||
|
||||
// Wait for the restart to finish and actually check if it was
|
||||
// successful or not.
|
||||
result := <-restartChan
|
||||
|
||||
switch result {
|
||||
case "done":
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("restarting systemd unit %q failed: expected %v but received %v", unit, "done", result)
|
||||
}
|
||||
}
|
||||
|
||||
func startSystemdUnit(ctx context.Context, unit string) error {
|
||||
conn, err := dbus.NewSystemdConnectionContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("establishing systemd connection: %w", err)
|
||||
}
|
||||
|
||||
startChan := make(chan string)
|
||||
if _, err := conn.StartUnitContext(ctx, unit, "replace", startChan); err != nil {
|
||||
return fmt.Errorf("starting systemd unit %q: %w", unit, err)
|
||||
}
|
||||
|
||||
// Wait for the enable to finish and actually check if it was
|
||||
// successful or not.
|
||||
result := <-startChan
|
||||
|
||||
switch result {
|
||||
case "done":
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("starting systemd unit %q failed: expected %v but received %v", unit, "done", result)
|
||||
}
|
||||
}
|
||||
|
||||
func enableSystemdUnit(ctx context.Context, unitPath string) error {
|
||||
conn, err := dbus.NewSystemdConnectionContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("establishing systemd connection: %w", err)
|
||||
}
|
||||
|
||||
if _, _, err := conn.EnableUnitFilesContext(ctx, []string{unitPath}, true, true); err != nil {
|
||||
return fmt.Errorf("enabling systemd unit %q: %w", unitPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
363
bootstrapper/internal/kubernetes/k8sapi/util.go
Normal file
363
bootstrapper/internal/kubernetes/k8sapi/util.go
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
const (
|
||||
// kubeConfig is the path to the Kubernetes admin config (used for authentication).
|
||||
kubeConfig = "/etc/kubernetes/admin.conf"
|
||||
// kubeletStartTimeout is the maximum time given to the kubelet service to (re)start.
|
||||
kubeletStartTimeout = 10 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
kubernetesKeyRegexp = regexp.MustCompile("[a-f0-9]{64}")
|
||||
providerIDRegex = regexp.MustCompile(`^azure:///subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachineScaleSets/([^/]+)/virtualMachines/([^/]+)$`)
|
||||
)
|
||||
|
||||
// Client provides the functionality of `kubectl apply`.
|
||||
type Client interface {
|
||||
Apply(resources resources.Marshaler, forceConflicts bool) error
|
||||
SetKubeconfig(kubeconfig []byte)
|
||||
// TODO: add tolerations
|
||||
}
|
||||
|
||||
type ClusterUtil interface {
|
||||
InstallComponents(ctx context.Context, version string) error
|
||||
InitCluster(initConfig []byte) error
|
||||
JoinCluster(joinConfig []byte) error
|
||||
SetupPodNetwork(kubectl Client, podNetworkConfiguration resources.Marshaler) error
|
||||
SetupAccessManager(kubectl Client, accessManagerConfiguration resources.Marshaler) error
|
||||
SetupAutoscaling(kubectl Client, clusterAutoscalerConfiguration resources.Marshaler, secrets resources.Marshaler) error
|
||||
SetupCloudControllerManager(kubectl Client, cloudControllerManagerConfiguration resources.Marshaler, configMaps resources.Marshaler, secrets resources.Marshaler) error
|
||||
SetupCloudNodeManager(kubectl Client, cloudNodeManagerConfiguration resources.Marshaler) error
|
||||
SetupKMS(kubectl Client, kmsConfiguration resources.Marshaler) error
|
||||
StartKubelet() error
|
||||
RestartKubelet() error
|
||||
GetControlPlaneJoinCertificateKey() (string, error)
|
||||
CreateJoinToken(ttl time.Duration) (*kubeadm.BootstrapTokenDiscovery, error)
|
||||
}
|
||||
|
||||
// KubernetesUtil provides low level management of the kubernetes cluster.
|
||||
type KubernetesUtil struct {
|
||||
inst installer
|
||||
}
|
||||
|
||||
// NewKubernetesUtils creates a new KubernetesUtil.
|
||||
func NewKubernetesUtil() *KubernetesUtil {
|
||||
return &KubernetesUtil{
|
||||
inst: newOSInstaller(),
|
||||
}
|
||||
}
|
||||
|
||||
// InstallComponents installs kubernetes components in the version specified.
|
||||
func (k *KubernetesUtil) InstallComponents(ctx context.Context, version string) error {
|
||||
var versionConf kubernetesVersion
|
||||
var ok bool
|
||||
if versionConf, ok = versionConfigs[version]; !ok {
|
||||
return fmt.Errorf("unsupported kubernetes version %q", version)
|
||||
}
|
||||
if err := versionConf.installK8sComponents(ctx, k.inst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return enableSystemdUnit(ctx, kubeletServiceEtcPath)
|
||||
}
|
||||
|
||||
func (k *KubernetesUtil) InitCluster(ctx context.Context, initConfig []byte) error {
|
||||
// TODO: audit policy should be user input
|
||||
auditPolicy, err := resources.NewDefaultAuditPolicy().Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating default audit policy: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(auditPolicyPath, auditPolicy, 0o644); err != nil {
|
||||
return fmt.Errorf("writing default audit policy: %w", err)
|
||||
}
|
||||
|
||||
initConfigFile, err := os.CreateTemp("", "kubeadm-init.*.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating init config file %v: %w", initConfigFile.Name(), err)
|
||||
}
|
||||
defer os.Remove(initConfigFile.Name())
|
||||
|
||||
if _, err := initConfigFile.Write(initConfig); err != nil {
|
||||
return fmt.Errorf("writing kubeadm init yaml config %v: %w", initConfigFile.Name(), err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, kubeadmPath, "init", "--config", initConfigFile.Name())
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return fmt.Errorf("kubeadm init failed (code %v) with: %s", exitErr.ExitCode(), exitErr.Stderr)
|
||||
}
|
||||
return fmt.Errorf("kubeadm init: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetupPodNetworkInput struct {
|
||||
CloudProvider string
|
||||
NodeName string
|
||||
FirstNodePodCIDR string
|
||||
SubnetworkPodCIDR string
|
||||
ProviderID string
|
||||
}
|
||||
|
||||
// SetupPodNetwork sets up the cilium pod network.
|
||||
func (k *KubernetesUtil) SetupPodNetwork(ctx context.Context, in SetupPodNetworkInput) error {
|
||||
switch in.CloudProvider {
|
||||
case "gcp":
|
||||
return k.setupGCPPodNetwork(ctx, in.NodeName, in.FirstNodePodCIDR, in.SubnetworkPodCIDR)
|
||||
case "azure":
|
||||
return k.setupAzurePodNetwork(ctx, in.ProviderID, in.SubnetworkPodCIDR)
|
||||
case "qemu":
|
||||
return k.setupQemuPodNetwork(ctx, in.SubnetworkPodCIDR)
|
||||
default:
|
||||
return fmt.Errorf("unsupported cloud provider %q", in.CloudProvider)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesUtil) setupAzurePodNetwork(ctx context.Context, providerID, subnetworkPodCIDR string) error {
|
||||
matches := providerIDRegex.FindStringSubmatch(providerID)
|
||||
if len(matches) != 5 {
|
||||
return fmt.Errorf("error splitting providerID %q", providerID)
|
||||
}
|
||||
|
||||
ciliumInstall := exec.CommandContext(ctx, "cilium", "install", "--azure-resource-group", matches[2], "--ipam", "azure",
|
||||
"--helm-set",
|
||||
"tunnel=disabled,enableIPv4Masquerade=true,azure.enabled=true,debug.enabled=true,ipv4NativeRoutingCIDR="+subnetworkPodCIDR+
|
||||
",endpointRoutes.enabled=true,encryption.enabled=true,encryption.type=wireguard,l7Proxy=false,egressMasqueradeInterfaces=eth0")
|
||||
ciliumInstall.Env = append(os.Environ(), "KUBECONFIG="+kubeConfig)
|
||||
out, err := ciliumInstall.CombinedOutput()
|
||||
if err != nil {
|
||||
err = errors.New(string(out))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubernetesUtil) setupGCPPodNetwork(ctx context.Context, nodeName, nodePodCIDR, subnetworkPodCIDR string) error {
|
||||
out, err := exec.CommandContext(ctx, kubectlPath, "--kubeconfig", kubeConfig, "patch", "node", nodeName, "-p", "{\"spec\":{\"podCIDR\": \""+nodePodCIDR+"\"}}").CombinedOutput()
|
||||
if err != nil {
|
||||
err = errors.New(string(out))
|
||||
return err
|
||||
}
|
||||
|
||||
// allow coredns to run on uninitialized nodes (required by cloud-controller-manager)
|
||||
err = exec.CommandContext(ctx, kubectlPath, "--kubeconfig", kubeConfig, "-n", "kube-system", "patch", "deployment", "coredns", "--type", "json", "-p", "[{\"op\":\"add\",\"path\":\"/spec/template/spec/tolerations/-\",\"value\":{\"key\":\"node.cloudprovider.kubernetes.io/uninitialized\",\"value\":\"true\",\"effect\":\"NoSchedule\"}},{\"op\":\"add\",\"path\":\"/spec/template/spec/nodeSelector\",\"value\":{\"node-role.kubernetes.io/control-plane\":\"\"}}]").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ciliumInstall := exec.CommandContext(ctx, "cilium", "install", "--ipam", "kubernetes", "--ipv4-native-routing-cidr", subnetworkPodCIDR,
|
||||
"--helm-set", "endpointRoutes.enabled=true,tunnel=disabled,encryption.enabled=true,encryption.type=wireguard,l7Proxy=false")
|
||||
ciliumInstall.Env = append(os.Environ(), "KUBECONFIG="+kubeConfig)
|
||||
out, err = ciliumInstall.CombinedOutput()
|
||||
if err != nil {
|
||||
err = errors.New(string(out))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FixCilium fixes https://github.com/cilium/cilium/issues/19958 but instead of a rollout restart of
|
||||
// the cilium daemonset, it only restarts the local cilium pod.
|
||||
func (k *KubernetesUtil) FixCilium(nodeNameK8s string) {
|
||||
// wait for cilium pod to be healthy
|
||||
for {
|
||||
time.Sleep(5 * time.Second)
|
||||
resp, err := http.Get("http://127.0.0.1:9876/healthz")
|
||||
if err != nil {
|
||||
fmt.Printf("waiting for local cilium daemonset pod not healthy: %v\n", err)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// get cilium pod name
|
||||
out, err := exec.CommandContext(context.Background(), "/bin/bash", "-c", "/run/state/bin/crictl ps -o json | jq -r '.containers[] | select(.metadata.name == \"cilium-agent\") | .podSandboxId'").CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("getting pod id failed: %v: %v\n", err, string(out))
|
||||
return
|
||||
}
|
||||
outLines := strings.Split(string(out), "\n")
|
||||
fmt.Println(outLines)
|
||||
podID := outLines[len(outLines)-2]
|
||||
|
||||
// stop and delete pod
|
||||
out, err = exec.CommandContext(context.Background(), "/run/state/bin/crictl", "stopp", podID).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("stopping cilium agent pod failed: %v: %v\n", err, string(out))
|
||||
return
|
||||
}
|
||||
out, err = exec.CommandContext(context.Background(), "/run/state/bin/crictl", "rmp", podID).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("removing cilium agent pod failed: %v: %v\n", err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KubernetesUtil) setupQemuPodNetwork(ctx context.Context, subnetworkPodCIDR string) error {
|
||||
ciliumInstall := exec.CommandContext(ctx, "cilium", "install", "--encryption", "wireguard", "--helm-set", "ipam.operator.clusterPoolIPv4PodCIDRList="+subnetworkPodCIDR+",endpointRoutes.enabled=true")
|
||||
ciliumInstall.Env = append(os.Environ(), "KUBECONFIG="+kubeConfig)
|
||||
out, err := ciliumInstall.CombinedOutput()
|
||||
if err != nil {
|
||||
err = errors.New(string(out))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupAutoscaling deploys the k8s cluster autoscaler.
|
||||
func (k *KubernetesUtil) SetupAutoscaling(kubectl Client, clusterAutoscalerConfiguration resources.Marshaler, secrets resources.Marshaler) error {
|
||||
if err := kubectl.Apply(secrets, true); err != nil {
|
||||
return fmt.Errorf("applying cluster-autoscaler Secrets: %w", err)
|
||||
}
|
||||
return kubectl.Apply(clusterAutoscalerConfiguration, true)
|
||||
}
|
||||
|
||||
// SetupActivationService deploys the Constellation node activation service.
|
||||
func (k *KubernetesUtil) SetupActivationService(kubectl Client, activationServiceConfiguration resources.Marshaler) error {
|
||||
return kubectl.Apply(activationServiceConfiguration, true)
|
||||
}
|
||||
|
||||
// SetupCloudControllerManager deploys the k8s cloud-controller-manager.
|
||||
func (k *KubernetesUtil) SetupCloudControllerManager(kubectl Client, cloudControllerManagerConfiguration resources.Marshaler, configMaps resources.Marshaler, secrets resources.Marshaler) error {
|
||||
if err := kubectl.Apply(configMaps, true); err != nil {
|
||||
return fmt.Errorf("applying ccm ConfigMaps: %w", err)
|
||||
}
|
||||
if err := kubectl.Apply(secrets, true); err != nil {
|
||||
return fmt.Errorf("applying ccm Secrets: %w", err)
|
||||
}
|
||||
if err := kubectl.Apply(cloudControllerManagerConfiguration, true); err != nil {
|
||||
return fmt.Errorf("applying ccm: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupCloudNodeManager deploys the k8s cloud-node-manager.
|
||||
func (k *KubernetesUtil) SetupCloudNodeManager(kubectl Client, cloudNodeManagerConfiguration resources.Marshaler) error {
|
||||
return kubectl.Apply(cloudNodeManagerConfiguration, true)
|
||||
}
|
||||
|
||||
// SetupAccessManager deploys the constellation-access-manager for deploying SSH keys on control-plane & worker nodes.
|
||||
func (k *KubernetesUtil) SetupAccessManager(kubectl Client, accessManagerConfiguration resources.Marshaler) error {
|
||||
return kubectl.Apply(accessManagerConfiguration, true)
|
||||
}
|
||||
|
||||
// SetupKMS deploys the KMS deployment.
|
||||
func (k *KubernetesUtil) SetupKMS(kubectl Client, kmsConfiguration resources.Marshaler) error {
|
||||
if err := kubectl.Apply(kmsConfiguration, true); err != nil {
|
||||
return fmt.Errorf("applying KMS configuration: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupVerificationService deploys the verification service.
|
||||
func (k *KubernetesUtil) SetupVerificationService(kubectl Client, verificationServiceConfiguration resources.Marshaler) error {
|
||||
return kubectl.Apply(verificationServiceConfiguration, true)
|
||||
}
|
||||
|
||||
// JoinCluster joins existing Kubernetes cluster using kubeadm join.
|
||||
func (k *KubernetesUtil) JoinCluster(ctx context.Context, joinConfig []byte) error {
|
||||
// TODO: audit policy should be user input
|
||||
auditPolicy, err := resources.NewDefaultAuditPolicy().Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating default audit policy: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(auditPolicyPath, auditPolicy, 0o644); err != nil {
|
||||
return fmt.Errorf("writing default audit policy: %w", err)
|
||||
}
|
||||
|
||||
joinConfigFile, err := os.CreateTemp("", "kubeadm-join.*.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating join config file %v: %w", joinConfigFile.Name(), err)
|
||||
}
|
||||
defer os.Remove(joinConfigFile.Name())
|
||||
|
||||
if _, err := joinConfigFile.Write(joinConfig); err != nil {
|
||||
return fmt.Errorf("writing kubeadm init yaml config %v: %w", joinConfigFile.Name(), err)
|
||||
}
|
||||
|
||||
// run `kubeadm join` to join a worker node to an existing Kubernetes cluster
|
||||
cmd := exec.CommandContext(ctx, kubeadmPath, "join", "--config", joinConfigFile.Name())
|
||||
if _, err := cmd.Output(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return fmt.Errorf("kubeadm join failed (code %v) with: %s", exitErr.ExitCode(), exitErr.Stderr)
|
||||
}
|
||||
return fmt.Errorf("kubeadm join: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartKubelet enables and starts the kubelet systemd unit.
|
||||
func (k *KubernetesUtil) StartKubelet() error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), kubeletStartTimeout)
|
||||
defer cancel()
|
||||
if err := enableSystemdUnit(ctx, kubeletServiceEtcPath); err != nil {
|
||||
return fmt.Errorf("enabling kubelet systemd unit: %w", err)
|
||||
}
|
||||
return startSystemdUnit(ctx, "kubelet.service")
|
||||
}
|
||||
|
||||
// RestartKubelet restarts a kubelet.
|
||||
func (k *KubernetesUtil) RestartKubelet() error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), kubeletStartTimeout)
|
||||
defer cancel()
|
||||
return restartSystemdUnit(ctx, "kubelet.service")
|
||||
}
|
||||
|
||||
// GetControlPlaneJoinCertificateKey return the key which can be used in combination with the joinArgs
|
||||
// to join the Cluster as control-plane.
|
||||
func (k *KubernetesUtil) GetControlPlaneJoinCertificateKey(ctx context.Context) (string, error) {
|
||||
// Key will be valid for 1h (no option to reduce the duration).
|
||||
// https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init-phase/#cmd-phase-upload-certs
|
||||
output, err := exec.CommandContext(ctx, kubeadmPath, "init", "phase", "upload-certs", "--upload-certs").Output()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return "", fmt.Errorf("kubeadm upload-certs failed (code %v) with: %s", exitErr.ExitCode(), exitErr.Stderr)
|
||||
}
|
||||
return "", fmt.Errorf("kubeadm upload-certs: %w", err)
|
||||
}
|
||||
// Example output:
|
||||
/*
|
||||
[upload-certs] Storing the certificates in ConfigMap "kubeadm-certs" in the "kube-system" Namespace
|
||||
[upload-certs] Using certificate key:
|
||||
9555b74008f24687eb964bd90a164ecb5760a89481d9c55a77c129b7db438168
|
||||
*/
|
||||
key := kubernetesKeyRegexp.FindString(string(output))
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("failed to parse kubeadm output: %s", string(output))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// CreateJoinToken creates a new bootstrap (join) token.
|
||||
func (k *KubernetesUtil) CreateJoinToken(ctx context.Context, ttl time.Duration) (*kubeadm.BootstrapTokenDiscovery, error) {
|
||||
output, err := exec.CommandContext(ctx, kubeadmPath, "token", "create", "--ttl", ttl.String(), "--print-join-command").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kubeadm token create: %w", err)
|
||||
}
|
||||
// `kubeadm token create [...] --print-join-command` outputs the following format:
|
||||
// kubeadm join [API_SERVER_ENDPOINT] --token [TOKEN] --discovery-token-ca-cert-hash [DISCOVERY_TOKEN_CA_CERT_HASH]
|
||||
return ParseJoinCommand(string(output))
|
||||
}
|
||||
95
bootstrapper/internal/kubernetes/k8sapi/versions.go
Normal file
95
bootstrapper/internal/kubernetes/k8sapi/versions.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package k8sapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/icholy/replace"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
const (
|
||||
cniPluginsDir = "/opt/cni/bin"
|
||||
binDir = "/run/state/bin"
|
||||
kubeadmPath = "/run/state/bin/kubeadm"
|
||||
kubeletPath = "/run/state/bin/kubelet"
|
||||
kubectlPath = "/run/state/bin/kubectl"
|
||||
kubeletServiceEtcPath = "/etc/systemd/system/kubelet.service"
|
||||
kubeletServiceStatePath = "/run/state/systemd/system/kubelet.service"
|
||||
kubeadmConfEtcPath = "/etc/systemd/system/kubelet.service.d/10-kubeadm.conf"
|
||||
kubeadmConfStatePath = "/run/state/systemd/system/kubelet.service.d/10-kubeadm.conf"
|
||||
executablePerm = 0o544
|
||||
systemdUnitPerm = 0o644
|
||||
)
|
||||
|
||||
// versionConfigs holds download URLs for all required kubernetes components for every supported version.
|
||||
var versionConfigs map[string]kubernetesVersion = map[string]kubernetesVersion{
|
||||
"1.23.6": {
|
||||
CNIPluginsURL: "https://github.com/containernetworking/plugins/releases/download/v1.1.1/cni-plugins-linux-amd64-v1.1.1.tgz",
|
||||
CrictlURL: "https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.24.1/crictl-v1.24.1-linux-amd64.tar.gz",
|
||||
KubeletServiceURL: "https://raw.githubusercontent.com/kubernetes/release/v0.13.0/cmd/kubepkg/templates/latest/deb/kubelet/lib/systemd/system/kubelet.service",
|
||||
KubeadmConfURL: "https://raw.githubusercontent.com/kubernetes/release/v0.13.0/cmd/kubepkg/templates/latest/deb/kubeadm/10-kubeadm.conf",
|
||||
KubeletURL: "https://storage.googleapis.com/kubernetes-release/release/v1.23.6/bin/linux/amd64/kubelet",
|
||||
KubeadmURL: "https://storage.googleapis.com/kubernetes-release/release/v1.23.6/bin/linux/amd64/kubeadm",
|
||||
KubectlURL: "https://storage.googleapis.com/kubernetes-release/release/v1.23.6/bin/linux/amd64/kubectl",
|
||||
},
|
||||
}
|
||||
|
||||
type kubernetesVersion struct {
|
||||
CNIPluginsURL string
|
||||
CrictlURL string
|
||||
KubeletServiceURL string
|
||||
KubeadmConfURL string
|
||||
KubeletURL string
|
||||
KubeadmURL string
|
||||
KubectlURL string
|
||||
}
|
||||
|
||||
// installK8sComponents installs kubernetes components for this version.
|
||||
// reference: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl .
|
||||
func (k *kubernetesVersion) installK8sComponents(ctx context.Context, inst installer) error {
|
||||
if err := inst.Install(
|
||||
ctx, k.CNIPluginsURL, []string{cniPluginsDir}, executablePerm, true,
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing cni plugins: %w", err)
|
||||
}
|
||||
if err := inst.Install(
|
||||
ctx, k.CrictlURL, []string{binDir}, executablePerm, true,
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing crictl: %w", err)
|
||||
}
|
||||
if err := inst.Install(
|
||||
ctx, k.KubeletServiceURL, []string{kubeletServiceEtcPath, kubeletServiceStatePath}, systemdUnitPerm, false, replace.String("/usr/bin", binDir),
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing kubelet service: %w", err)
|
||||
}
|
||||
if err := inst.Install(
|
||||
ctx, k.KubeadmConfURL, []string{kubeadmConfEtcPath, kubeadmConfStatePath}, systemdUnitPerm, false, replace.String("/usr/bin", binDir),
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing kubeadm conf: %w", err)
|
||||
}
|
||||
if err := inst.Install(
|
||||
ctx, k.KubeletURL, []string{kubeletPath}, executablePerm, false,
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing kubelet: %w", err)
|
||||
}
|
||||
if err := inst.Install(
|
||||
ctx, k.KubeadmURL, []string{kubeadmPath}, executablePerm, false,
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing kubeadm: %w", err)
|
||||
}
|
||||
if err := inst.Install(
|
||||
ctx, k.KubectlURL, []string{kubectlPath}, executablePerm, false,
|
||||
); err != nil {
|
||||
return fmt.Errorf("installing kubectl: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type installer interface {
|
||||
Install(
|
||||
ctx context.Context, sourceURL string, destinations []string, perm fs.FileMode,
|
||||
extract bool, transforms ...transform.Transformer,
|
||||
) error
|
||||
}
|
||||
29
bootstrapper/internal/kubernetes/k8sutil.go
Normal file
29
bootstrapper/internal/kubernetes/k8sutil.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi"
|
||||
"github.com/edgelesssys/constellation/bootstrapper/internal/kubernetes/k8sapi/resources"
|
||||
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
type clusterUtil interface {
|
||||
InstallComponents(ctx context.Context, version string) error
|
||||
InitCluster(ctx context.Context, initConfig []byte) error
|
||||
JoinCluster(ctx context.Context, joinConfig []byte) error
|
||||
SetupPodNetwork(context.Context, k8sapi.SetupPodNetworkInput) error
|
||||
SetupAccessManager(kubectl k8sapi.Client, sshUsers resources.Marshaler) error
|
||||
SetupAutoscaling(kubectl k8sapi.Client, clusterAutoscalerConfiguration resources.Marshaler, secrets resources.Marshaler) error
|
||||
SetupActivationService(kubectl k8sapi.Client, activationServiceConfiguration resources.Marshaler) error
|
||||
SetupCloudControllerManager(kubectl k8sapi.Client, cloudControllerManagerConfiguration resources.Marshaler, configMaps resources.Marshaler, secrets resources.Marshaler) error
|
||||
SetupCloudNodeManager(kubectl k8sapi.Client, cloudNodeManagerConfiguration resources.Marshaler) error
|
||||
SetupKMS(kubectl k8sapi.Client, kmsConfiguration resources.Marshaler) error
|
||||
SetupVerificationService(kubectl k8sapi.Client, verificationServiceConfiguration resources.Marshaler) error
|
||||
StartKubelet() error
|
||||
RestartKubelet() error
|
||||
GetControlPlaneJoinCertificateKey(ctx context.Context) (string, error)
|
||||
CreateJoinToken(ctx context.Context, ttl time.Duration) (*kubeadm.BootstrapTokenDiscovery, error)
|
||||
FixCilium(nodeNameK8s string)
|
||||
}
|
||||
23
bootstrapper/internal/kubernetes/kubeconfig.go
Normal file
23
bootstrapper/internal/kubernetes/kubeconfig.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const kubeconfigPath = "/etc/kubernetes/admin.conf"
|
||||
|
||||
// KubeconfigReader implements ConfigReader.
|
||||
type KubeconfigReader struct {
|
||||
fs afero.Afero
|
||||
}
|
||||
|
||||
// ReadKubeconfig reads the Kubeconfig from disk.
|
||||
func (r KubeconfigReader) ReadKubeconfig() ([]byte, error) {
|
||||
kubeconfig, err := r.fs.ReadFile(kubeconfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading gce config: %w", err)
|
||||
}
|
||||
return kubeconfig, nil
|
||||
}
|
||||
34
bootstrapper/internal/kubernetes/kubeconfig_test.go
Normal file
34
bootstrapper/internal/kubernetes/kubeconfig_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadKubeconfig(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
fs := afero.Afero{
|
||||
Fs: afero.NewMemMapFs(),
|
||||
}
|
||||
require.NoError(fs.WriteFile(kubeconfigPath, []byte("someConfig"), 0o644))
|
||||
reader := KubeconfigReader{fs}
|
||||
config, err := reader.ReadKubeconfig()
|
||||
|
||||
require.NoError(err)
|
||||
assert.Equal([]byte("someConfig"), config)
|
||||
}
|
||||
|
||||
func TestReadKubeconfigFails(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
fs := afero.Afero{
|
||||
Fs: afero.NewMemMapFs(),
|
||||
}
|
||||
reader := KubeconfigReader{fs}
|
||||
_, err := reader.ReadKubeconfig()
|
||||
|
||||
assert.Error(err)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue