Move cloud metadata packages and kubernetes resources marshaling to internal

Decouples cloud provider metadata packages from kubernetes related code

Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Malte Poll 2022-08-29 14:30:20 +02:00 committed by Malte Poll
parent 89e3acf6a1
commit 26e9c67a00
81 changed files with 169 additions and 145 deletions

View file

@ -0,0 +1,70 @@
package azure
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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 virtualNetworksAPI interface {
NewListPager(resourceGroupName string, options *armnetwork.VirtualNetworksClientListOptions) *runtime.Pager[armnetwork.VirtualNetworksClientListResponse]
}
type securityGroupsAPI interface {
NewListPager(resourceGroupName string, options *armnetwork.SecurityGroupsClientListOptions) *runtime.Pager[armnetwork.SecurityGroupsClientListResponse]
}
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 *armcomputev2.VirtualMachineScaleSetVMsClientGetOptions,
) (armcomputev2.VirtualMachineScaleSetVMsClientGetResponse, error)
NewListPager(resourceGroupName string, virtualMachineScaleSetName string,
options *armcomputev2.VirtualMachineScaleSetVMsClientListOptions,
) *runtime.Pager[armcomputev2.VirtualMachineScaleSetVMsClientListResponse]
}
type scaleSetsAPI interface {
NewListPager(resourceGroupName string, options *armcomputev2.VirtualMachineScaleSetsClientListOptions,
) *runtime.Pager[armcomputev2.VirtualMachineScaleSetsClientListResponse]
}
type loadBalancerAPI interface {
NewListPager(resourceGroupName string, options *armnetwork.LoadBalancersClientListOptions,
) *runtime.Pager[armnetwork.LoadBalancersClientListResponse]
}
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)
}

View file

@ -0,0 +1,276 @@
package azure
import (
"context"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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{
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{
Interface: a.getInterface,
}, a.getErr
}
type stubVirtualMachineScaleSetVMsAPI struct {
getVM armcomputev2.VirtualMachineScaleSetVM
getErr error
pager *stubVirtualMachineScaleSetVMPager
}
func (a *stubVirtualMachineScaleSetVMsAPI) Get(ctx context.Context, resourceGroupName string, vmScaleSetName string, instanceID string, options *armcomputev2.VirtualMachineScaleSetVMsClientGetOptions) (armcomputev2.VirtualMachineScaleSetVMsClientGetResponse, error) {
return armcomputev2.VirtualMachineScaleSetVMsClientGetResponse{
VirtualMachineScaleSetVM: a.getVM,
}, a.getErr
}
func (a *stubVirtualMachineScaleSetVMsAPI) NewListPager(resourceGroupName string, virtualMachineScaleSetName string, options *armcomputev2.VirtualMachineScaleSetVMsClientListOptions) *runtime.Pager[armcomputev2.VirtualMachineScaleSetVMsClientListResponse] {
return runtime.NewPager(runtime.PagingHandler[armcomputev2.VirtualMachineScaleSetVMsClientListResponse]{
More: a.pager.moreFunc(),
Fetcher: a.pager.fetcherFunc(),
})
}
type stubVirtualMachineScaleSetsClientListPager struct {
list []armcomputev2.VirtualMachineScaleSet
fetchErr error
more bool
}
func (p *stubVirtualMachineScaleSetsClientListPager) moreFunc() func(armcomputev2.VirtualMachineScaleSetsClientListResponse) bool {
return func(armcomputev2.VirtualMachineScaleSetsClientListResponse) bool {
return p.more
}
}
func (p *stubVirtualMachineScaleSetsClientListPager) fetcherFunc() func(context.Context, *armcomputev2.VirtualMachineScaleSetsClientListResponse) (armcomputev2.VirtualMachineScaleSetsClientListResponse, error) {
return func(context.Context, *armcomputev2.VirtualMachineScaleSetsClientListResponse) (armcomputev2.VirtualMachineScaleSetsClientListResponse, error) {
page := make([]*armcomputev2.VirtualMachineScaleSet, len(p.list))
for i := range p.list {
page[i] = &p.list[i]
}
return armcomputev2.VirtualMachineScaleSetsClientListResponse{
VirtualMachineScaleSetListResult: armcomputev2.VirtualMachineScaleSetListResult{
Value: page,
},
}, p.fetchErr
}
}
type stubScaleSetsAPI struct {
pager *stubVirtualMachineScaleSetsClientListPager
}
func (a *stubScaleSetsAPI) NewListPager(resourceGroupName string, options *armcomputev2.VirtualMachineScaleSetsClientListOptions) *runtime.Pager[armcomputev2.VirtualMachineScaleSetsClientListResponse] {
return runtime.NewPager(runtime.PagingHandler[armcomputev2.VirtualMachineScaleSetsClientListResponse]{
More: a.pager.moreFunc(),
Fetcher: a.pager.fetcherFunc(),
})
}
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 {
list []armnetwork.SecurityGroup
fetchErr error
more bool
}
func (p *stubSecurityGroupsClientListPager) moreFunc() func(armnetwork.SecurityGroupsClientListResponse) bool {
return func(armnetwork.SecurityGroupsClientListResponse) bool {
return p.more
}
}
func (p *stubSecurityGroupsClientListPager) fetcherFunc() func(context.Context, *armnetwork.SecurityGroupsClientListResponse) (armnetwork.SecurityGroupsClientListResponse, error) {
return func(context.Context, *armnetwork.SecurityGroupsClientListResponse) (armnetwork.SecurityGroupsClientListResponse, error) {
page := make([]*armnetwork.SecurityGroup, len(p.list))
for i := range p.list {
page[i] = &p.list[i]
}
return armnetwork.SecurityGroupsClientListResponse{
SecurityGroupListResult: armnetwork.SecurityGroupListResult{
Value: page,
},
}, p.fetchErr
}
}
type stubSecurityGroupsAPI struct {
pager *stubSecurityGroupsClientListPager
}
func (a *stubSecurityGroupsAPI) NewListPager(resourceGroupName string, options *armnetwork.SecurityGroupsClientListOptions) *runtime.Pager[armnetwork.SecurityGroupsClientListResponse] {
return runtime.NewPager(runtime.PagingHandler[armnetwork.SecurityGroupsClientListResponse]{
More: a.pager.moreFunc(),
Fetcher: a.pager.fetcherFunc(),
})
}
type stubVirtualNetworksClientListPager struct {
list []armnetwork.VirtualNetwork
fetchErr error
more bool
}
func (p *stubVirtualNetworksClientListPager) moreFunc() func(armnetwork.VirtualNetworksClientListResponse) bool {
return func(armnetwork.VirtualNetworksClientListResponse) bool {
return p.more
}
}
func (p *stubVirtualNetworksClientListPager) fetcherFunc() func(context.Context, *armnetwork.VirtualNetworksClientListResponse) (armnetwork.VirtualNetworksClientListResponse, error) {
return func(context.Context, *armnetwork.VirtualNetworksClientListResponse) (armnetwork.VirtualNetworksClientListResponse, error) {
page := make([]*armnetwork.VirtualNetwork, len(p.list))
for i := range p.list {
page[i] = &p.list[i]
}
return armnetwork.VirtualNetworksClientListResponse{
VirtualNetworkListResult: armnetwork.VirtualNetworkListResult{
Value: page,
},
}, p.fetchErr
}
}
type stubVirtualNetworksAPI struct {
pager *stubVirtualNetworksClientListPager
}
func (a *stubVirtualNetworksAPI) NewListPager(resourceGroupName string, options *armnetwork.VirtualNetworksClientListOptions) *runtime.Pager[armnetwork.VirtualNetworksClientListResponse] {
return runtime.NewPager(runtime.PagingHandler[armnetwork.VirtualNetworksClientListResponse]{
More: a.pager.moreFunc(),
Fetcher: a.pager.fetcherFunc(),
})
}
type stubLoadBalancersAPI struct {
pager *stubLoadBalancersClientListPager
}
func (a *stubLoadBalancersAPI) NewListPager(resourceGroupName string, options *armnetwork.LoadBalancersClientListOptions,
) *runtime.Pager[armnetwork.LoadBalancersClientListResponse] {
return runtime.NewPager(runtime.PagingHandler[armnetwork.LoadBalancersClientListResponse]{
More: a.pager.moreFunc(),
Fetcher: a.pager.fetcherFunc(),
})
}
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
}
type stubVirtualMachineScaleSetVMPager struct {
list []armcomputev2.VirtualMachineScaleSetVM
fetchErr error
more bool
}
func (p *stubVirtualMachineScaleSetVMPager) moreFunc() func(armcomputev2.VirtualMachineScaleSetVMsClientListResponse) bool {
return func(armcomputev2.VirtualMachineScaleSetVMsClientListResponse) bool {
return p.more
}
}
func (p *stubVirtualMachineScaleSetVMPager) fetcherFunc() func(context.Context, *armcomputev2.VirtualMachineScaleSetVMsClientListResponse) (armcomputev2.VirtualMachineScaleSetVMsClientListResponse, error) {
return func(context.Context, *armcomputev2.VirtualMachineScaleSetVMsClientListResponse) (armcomputev2.VirtualMachineScaleSetVMsClientListResponse, error) {
page := make([]*armcomputev2.VirtualMachineScaleSetVM, len(p.list))
for i := range p.list {
page[i] = &p.list[i]
}
return armcomputev2.VirtualMachineScaleSetVMsClientListResponse{
VirtualMachineScaleSetVMListResult: armcomputev2.VirtualMachineScaleSetVMListResult{
Value: page,
},
}, p.fetchErr
}
}
type stubLoadBalancersClientListPager struct {
list []armnetwork.LoadBalancer
fetchErr error
more bool
}
func (p *stubLoadBalancersClientListPager) moreFunc() func(armnetwork.LoadBalancersClientListResponse) bool {
return func(armnetwork.LoadBalancersClientListResponse) bool {
return p.more
}
}
func (p *stubLoadBalancersClientListPager) fetcherFunc() func(context.Context, *armnetwork.LoadBalancersClientListResponse) (armnetwork.LoadBalancersClientListResponse, error) {
return func(context.Context, *armnetwork.LoadBalancersClientListResponse) (armnetwork.LoadBalancersClientListResponse, error) {
page := make([]*armnetwork.LoadBalancer, len(p.list))
for i := range p.list {
page[i] = &p.list[i]
}
return armnetwork.LoadBalancersClientListResponse{
LoadBalancerListResult: armnetwork.LoadBalancerListResult{
Value: page,
},
}, p.fetchErr
}
}

View file

@ -0,0 +1,123 @@
package azure
import (
"github.com/edgelesssys/constellation/internal/azureshared"
"github.com/edgelesssys/constellation/internal/kubernetes"
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) (kubernetes.Secrets, error) {
subscriptionID, resourceGroup, err := azureshared.BasicsFromProviderID(providerID)
if err != nil {
return kubernetes.Secrets{}, err
}
creds, err := azureshared.ApplicationCredentialsFromURI(cloudServiceAccountURI)
if err != nil {
return kubernetes.Secrets{}, err
}
return kubernetes.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
}

View file

@ -0,0 +1,81 @@
package azure
import (
"testing"
"github.com/edgelesssys/constellation/internal/kubernetes"
"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 kubernetes.Secrets
wantErr bool
}{
"Secrets works": {
providerID: "azure:///subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scaleset/virtualMachines/instance-name",
cloudServiceAccountURI: "serviceaccount://azure?tenant_id=tenant-id&client_id=client-id&client_secret=client-secret",
wantSecrets: kubernetes.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
internal/cloud/azure/ccm.go Normal file
View file

@ -0,0 +1,183 @@
package azure
import (
"context"
"encoding/json"
"github.com/edgelesssys/constellation/internal/azureshared"
"github.com/edgelesssys/constellation/internal/cloud/metadata"
"github.com/edgelesssys/constellation/internal/kubernetes"
"github.com/edgelesssys/constellation/internal/versions"
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(k8sVersion versions.ValidK8sVersion) (string, error) {
return versions.VersionConfigs[k8sVersion].CloudControllerManagerImageAzure, nil
}
// 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) (kubernetes.ConfigMaps, error) {
return kubernetes.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) (kubernetes.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 kubernetes.Secrets{}, err
}
creds, err := azureshared.ApplicationCredentialsFromURI(cloudServiceAccountURI)
if err != nil {
return kubernetes.Secrets{}, err
}
vmType := "standard"
if _, _, _, _, err := azureshared.ScaleSetInformationFromProviderID(providerID); err == nil {
vmType = "vmss"
}
securityGroupName, err := c.metadata.GetNetworkSecurityGroupName(ctx)
if err != nil {
return kubernetes.Secrets{}, err
}
loadBalancerName, err := c.metadata.GetLoadBalancerName(ctx)
if err != nil {
return kubernetes.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 kubernetes.Secrets{}, err
}
return kubernetes.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"`
}

View file

@ -0,0 +1,117 @@
package azure
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/internal/cloud/metadata"
"github.com/edgelesssys/constellation/internal/kubernetes"
"github.com/edgelesssys/constellation/internal/versions"
"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 kubernetes.Secrets
wantErr bool
}{
"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: kubernetes.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(versions.Latest))
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
}

View file

@ -0,0 +1,31 @@
package azure
import (
"github.com/edgelesssys/constellation/internal/versions"
)
// 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(k8sVersion versions.ValidK8sVersion) (string, error) {
return versions.VersionConfigs[k8sVersion].CloudNodeManagerImageAzure, nil
}
// 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
}

View file

@ -0,0 +1,18 @@
package azure
import (
"testing"
"github.com/edgelesssys/constellation/internal/versions"
"github.com/stretchr/testify/assert"
)
func TestTrivialCNMFunctions(t *testing.T) {
assert := assert.New(t)
cloud := CloudNodeManager{}
assert.NotEmpty(cloud.Image(versions.Latest))
assert.NotEmpty(cloud.Path())
assert.NotEmpty(cloud.ExtraArgs())
assert.True(cloud.Supported())
}

View 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"`
}

View 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)
}
})
}

View file

@ -0,0 +1,65 @@
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")
}
client := appinsights.NewTelemetryClient(*resp.Properties.InstrumentationKey)
instance, err := metadata.GetInstance(ctx, providerID)
if err != nil {
return nil, err
}
client.Context().CommonProperties["instance-name"] = instance.Name
return &Logger{
client: client,
}, 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
}

View file

@ -0,0 +1,348 @@
package azure
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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$`)
resourceGroupNameRegexp = regexp.MustCompile(`^(.*)-([^-]+)$`)
)
// 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, err := armnetwork.NewVirtualNetworksClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
networkInterfacesAPI, err := armnetwork.NewInterfacesClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
publicIPAddressesAPI, err := armnetwork.NewPublicIPAddressesClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
securityGroupsAPI, err := armnetwork.NewSecurityGroupsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
scaleSetsAPI, err := armcomputev2.NewVirtualMachineScaleSetsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
loadBalancerAPI, err := armnetwork.NewLoadBalancersClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
virtualMachineScaleSetVMsAPI, err := armcomputev2.NewVirtualMachineScaleSetVMsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
tagsAPI, err := armresources.NewTagsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
applicationInsightsAPI, err := armapplicationinsights.NewComponentsClient(subscriptionID, cred, nil)
if err != nil {
return nil, err
}
return &Metadata{
imdsAPI: &imdsAPI,
virtualNetworksAPI: virtualNetworksAPI,
networkInterfacesAPI: networkInterfacesAPI,
securityGroupsAPI: securityGroupsAPI,
publicIPAddressesAPI: publicIPAddressesAPI,
loadBalancerAPI: loadBalancerAPI,
scaleSetsAPI: scaleSetsAPI,
virtualMachineScaleSetVMsAPI: virtualMachineScaleSetVMsAPI,
tagsAPI: tagsAPI,
applicationInsightsAPI: 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
}
// UID retrieves the UID of the constellation.
func (m *Metadata) UID(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
}
uid, err := getUIDFromResourceGroup(resourceGroup)
if err != nil {
return "", err
}
return uid, 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.NewListPager(resourceGroup, nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("retrieving loadbalancer page: %w", err)
}
for _, lb := range page.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
}
// GetLoadBalancerEndpoint retrieves the first load balancer IP from cloud provider metadata.
//
// The returned string is an IP address without a port, but the method name needs to satisfy the
// metadata interface.
func (m *Metadata) GetLoadBalancerEndpoint(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 armcomputev2.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
}
func getUIDFromResourceGroup(resourceGroup string) (string, error) {
matches := resourceGroupNameRegexp.FindStringSubmatch(resourceGroup)
if len(matches) != 3 {
return "", errors.New("error splitting resource group name")
}
return matches[2], nil
}

View file

@ -0,0 +1,761 @@
package azure
import (
"context"
"errors"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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",
VPCIP: "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: newScaleSetIMDSStub(),
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: newScaleSetIMDSStub(),
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) {
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",
VPCIP: "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 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: newScaleSetIMDSStub(),
virtualMachineScaleSetVMsAPI: &stubVirtualMachineScaleSetVMsAPI{getErr: errors.New("failed")},
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: newScaleSetIMDSStub(),
securityGroupsAPI: &stubSecurityGroupsAPI{
pager: &stubSecurityGroupsClientListPager{
list: []armnetwork.SecurityGroup{{Name: to.Ptr(name)}},
},
},
wantName: name,
},
"no security group": {
imdsAPI: newScaleSetIMDSStub(),
securityGroupsAPI: &stubSecurityGroupsAPI{
pager: &stubSecurityGroupsClientListPager{},
},
wantErr: true,
},
"missing name in security group struct": {
imdsAPI: newScaleSetIMDSStub(),
securityGroupsAPI: &stubSecurityGroupsAPI{
pager: &stubSecurityGroupsClientListPager{
list: []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: newScaleSetIMDSStub(),
virtualNetworksAPI: &stubVirtualNetworksAPI{
pager: &stubVirtualNetworksClientListPager{
list: []armnetwork.VirtualNetwork{{
Name: to.Ptr(name),
Properties: &armnetwork.VirtualNetworkPropertiesFormat{
Subnets: []*armnetwork.Subnet{
{Properties: &armnetwork.SubnetPropertiesFormat{AddressPrefix: to.Ptr(subnetworkCIDR)}},
},
},
}},
},
},
wantNetworkCIDR: subnetworkCIDR,
},
"no virtual networks found": {
imdsAPI: newScaleSetIMDSStub(),
virtualNetworksAPI: &stubVirtualNetworksAPI{
pager: &stubVirtualNetworksClientListPager{},
},
wantErr: true,
wantNetworkCIDR: subnetworkCIDR,
},
"malformed network struct": {
imdsAPI: newScaleSetIMDSStub(),
virtualNetworksAPI: &stubVirtualNetworksAPI{
pager: &stubVirtualNetworksClientListPager{list: []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: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{{
Name: to.Ptr(loadBalancerName),
Properties: &armnetwork.LoadBalancerPropertiesFormat{},
}},
},
},
wantName: loadBalancerName,
},
"invalid load balancer struct": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{list: []armnetwork.LoadBalancer{{}}},
},
wantErr: true,
},
"invalid missing name": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{list: []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 TestGetLoadBalancerEndpoint(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
}{
"GetLoadBalancerEndpoint works": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{{
Name: to.Ptr(loadBalancerName),
Properties: &armnetwork.LoadBalancerPropertiesFormat{
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
{
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PublicIPAddress: &armnetwork.PublicIPAddress{ID: &correctPublicIPID},
},
},
},
},
}},
},
},
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getResponse: armnetwork.PublicIPAddressesClientGetResponse{
PublicIPAddress: armnetwork.PublicIPAddress{
Properties: &armnetwork.PublicIPAddressPropertiesFormat{
IPAddress: &publicIP,
},
},
}},
wantIP: publicIP,
},
"no load balancer": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{},
},
wantErr: true,
},
"load balancer missing public IP reference": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{{
Name: to.Ptr(loadBalancerName),
Properties: &armnetwork.LoadBalancerPropertiesFormat{
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{},
},
}},
},
},
wantErr: true,
},
"public IP reference has wrong format": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{{
Name: to.Ptr(loadBalancerName),
Properties: &armnetwork.LoadBalancerPropertiesFormat{
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
{
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PublicIPAddress: &armnetwork.PublicIPAddress{
ID: to.Ptr("wrong-format"),
},
},
},
},
},
}},
},
},
wantErr: true,
},
"no public IP address found": {
imdsAPI: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{{
Name: to.Ptr(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: newScaleSetIMDSStub(),
loadBalancerAPI: &stubLoadBalancersAPI{
pager: &stubLoadBalancersClientListPager{
list: []armnetwork.LoadBalancer{{
Name: to.Ptr(loadBalancerName),
Properties: &armnetwork.LoadBalancerPropertiesFormat{
FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{
{
Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{
PublicIPAddress: &armnetwork.PublicIPAddress{ID: &correctPublicIPID},
},
},
},
},
}},
},
},
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getResponse: armnetwork.PublicIPAddressesClientGetResponse{
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.GetLoadBalancerEndpoint(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 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 TestUID(t *testing.T) {
testCases := map[string]struct {
imdsAPI imdsAPI
wantErr bool
wantUID string
}{
"uid extraction from providerID works": {
imdsAPI: &stubIMDSAPI{
res: metadataResponse{Compute: struct {
ResourceID string `json:"resourceId,omitempty"`
}{"/subscriptions/subscription-id/resourceGroups/basename-uid/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"}},
},
wantUID: "uid",
},
"providerID does not contain uid": {
imdsAPI: &stubIMDSAPI{
res: metadataResponse{Compute: struct {
ResourceID string `json:"resourceId,omitempty"`
}{"/subscriptions/subscription-id/resourceGroups/invalid/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"}},
},
wantErr: true,
},
"providerID is invalid": {
imdsAPI: newInvalidIMDSStub(),
wantErr: true,
},
"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,
}
uid, err := metadata.UID(context.Background())
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantUID, uid)
})
}
}
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.Ptr("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 armcomputev2.SSHConfiguration
wantKeys map[string][]string
}{
"ssh key is extracted": {
in: armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
KeyData: to.Ptr("key-data"),
Path: to.Ptr("/home/user/.ssh/authorized_keys"),
},
},
},
wantKeys: map[string][]string{"user": {"key-data"}},
},
"invalid path is skipped": {
in: armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
KeyData: to.Ptr("key-data"),
Path: to.Ptr("invalid-path"),
},
},
},
wantKeys: map[string][]string{},
},
"key data is nil": {
in: armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
Path: to.Ptr("/home/user/.ssh/authorized_keys"),
},
},
},
wantKeys: map[string][]string{},
},
"path is nil": {
in: armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
KeyData: to.Ptr("key-data"),
},
},
},
wantKeys: map[string][]string{},
},
"public keys are nil": {
in: armcomputev2.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 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.Ptr("interface-name"),
Properties: &armnetwork.InterfacePropertiesFormat{
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.Ptr("192.0.2.0"),
Primary: to.Ptr(true),
},
},
},
},
},
}
}
func newScaleSetsStub() *stubScaleSetsAPI {
return &stubScaleSetsAPI{
pager: &stubVirtualMachineScaleSetsClientListPager{
list: []armcomputev2.VirtualMachineScaleSet{{
Name: to.Ptr("scale-set-name"),
}},
},
}
}
func newVirtualMachineScaleSetsVMsStub() *stubVirtualMachineScaleSetVMsAPI {
return &stubVirtualMachineScaleSetVMsAPI{
getVM: armcomputev2.VirtualMachineScaleSetVM{
Name: to.Ptr("scale-set-name_instance-id"),
InstanceID: to.Ptr("instance-id"),
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
NetworkProfile: &armcomputev2.NetworkProfile{
NetworkInterfaces: []*armcomputev2.NetworkInterfaceReference{
{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
},
},
},
OSProfile: &armcomputev2.OSProfile{
ComputerName: to.Ptr("scale-set-name-instance-id"),
LinuxConfiguration: &armcomputev2.LinuxConfiguration{
SSH: &armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
KeyData: to.Ptr("key-data"),
Path: to.Ptr("/home/user/.ssh/authorized_keys"),
},
},
},
},
},
},
},
pager: &stubVirtualMachineScaleSetVMPager{
list: []armcomputev2.VirtualMachineScaleSetVM{{
Name: to.Ptr("scale-set-name_instance-id"),
InstanceID: to.Ptr("instance-id"),
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
NetworkProfile: &armcomputev2.NetworkProfile{
NetworkInterfaces: []*armcomputev2.NetworkInterfaceReference{
{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
},
},
},
OSProfile: &armcomputev2.OSProfile{
ComputerName: to.Ptr("scale-set-name-instance-id"),
LinuxConfiguration: &armcomputev2.LinuxConfiguration{
SSH: &armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
KeyData: to.Ptr("key-data"),
Path: to.Ptr("/home/user/.ssh/authorized_keys"),
},
},
},
},
},
},
}},
},
}
}
func newFailingListsVirtualMachineScaleSetsVMsStub() *stubVirtualMachineScaleSetVMsAPI {
return &stubVirtualMachineScaleSetVMsAPI{
pager: &stubVirtualMachineScaleSetVMPager{
list: []armcomputev2.VirtualMachineScaleSetVM{{
InstanceID: to.Ptr("invalid-instance-id"),
}},
},
}
}
func newTagsStub() *stubTagsAPI {
return &stubTagsAPI{}
}

View file

@ -0,0 +1,116 @@
package azure
import (
"context"
"errors"
"fmt"
"strings"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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 armcomputev2.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 armcomputev2.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
}
// getScaleSetVMPublicIPAddress retrieves the primary public IP address from a network interface which is referenced by a scale set virtual machine.
func (m *Metadata) getScaleSetVMPublicIPAddress(ctx context.Context, resourceGroup, scaleSet, instanceID string,
networkInterfaces []armnetwork.Interface,
) (string, error) {
for _, networkInterface := range networkInterfaces {
if networkInterface.Properties == nil || networkInterface.Name == nil {
continue
}
for _, config := range networkInterface.Properties.IPConfigurations {
if config == nil || config.Name == nil || config.Properties == nil || config.Properties.PublicIPAddress == nil ||
config.Properties.Primary == nil || !*config.Properties.Primary {
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 "", fmt.Errorf("failed to retrieve public ip address %v: %w", publicIPAddressName, err)
}
if publicIPAddress.Properties == nil || publicIPAddress.Properties.IPAddress == nil {
return "", errors.New("retrieved public ip address has invalid ip address")
}
return *publicIPAddress.Properties.IPAddress, nil
}
}
// instances may have no public IP, in that case we don't return an error.
return "", nil
}
// extractVPCIP extracts the primary VPC IP from a list of network interface IP configurations.
func extractVPCIP(networkInterfaces []armnetwork.Interface) 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 || config.Properties.Primary == nil {
continue
}
if *config.Properties.Primary {
return *config.Properties.PrivateIPAddress
}
}
}
return ""
}
// 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 []*armcomputev2.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
}

View file

@ -0,0 +1,399 @@
package azure
import (
"context"
"errors"
"testing"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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 := armcomputev2.VirtualMachine{
Properties: &armcomputev2.VirtualMachineProperties{
NetworkProfile: &armcomputev2.NetworkProfile{
NetworkInterfaces: []*armcomputev2.NetworkInterfaceReference{
{
ID: to.StringPtr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/interface-name"),
},
},
},
},
}
testCases := map[string]struct {
vm armcomputev2.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: armcomputev2.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 := armcomputev2.VirtualMachineScaleSetVM{
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
NetworkProfile: &armcomputev2.NetworkProfile{
NetworkInterfaces: []*armcomputev2.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 armcomputev2.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: armcomputev2.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{
Primary: to.BoolPtr(true),
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
wantIP string
wantErr bool
}{
"retrieval works": {
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getVirtualMachineScaleSetPublicIPAddressResponse: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse{
PublicIPAddress: armnetwork.PublicIPAddress{
Properties: &armnetwork.PublicIPAddressPropertiesFormat{
IPAddress: to.StringPtr("192.0.2.1"),
},
},
}},
networkInterfaces: newNetworkInterfaces(),
wantIP: "192.0.2.1",
},
"retrieval works for no valid interfaces": {
publicIPAddressesAPI: &stubPublicIPAddressesAPI{getVirtualMachineScaleSetPublicIPAddressResponse: armnetwork.PublicIPAddressesClientGetVirtualMachineScaleSetPublicIPAddressResponse{
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{
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.getScaleSetVMPublicIPAddress(context.Background(), "resource-group", "scale-set-name", "instance-id", tc.networkInterfaces)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantIP, ips)
})
}
}
func TestExtractPrivateIPs(t *testing.T) {
testCases := map[string]struct {
networkInterfaces []armnetwork.Interface
wantIP string
}{
"extraction works": {
networkInterfaces: []armnetwork.Interface{
{
Properties: &armnetwork.InterfacePropertiesFormat{
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
Primary: to.BoolPtr(true),
PrivateIPAddress: to.StringPtr("192.0.2.0"),
},
},
{
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
PrivateIPAddress: to.StringPtr("192.0.2.1"),
},
},
},
},
},
},
wantIP: "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)
ip := extractVPCIP(tc.networkInterfaces)
assert.Equal(tc.wantIP, ip)
})
}
}
func TestExtractInterfaceNamesFromInterfaceReferences(t *testing.T) {
testCases := map[string]struct {
references []*armcomputev2.NetworkInterfaceReference
wantNames []string
}{
"extraction with individual interface reference works": {
references: []*armcomputev2.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: []*armcomputev2.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: []*armcomputev2.NetworkInterfaceReference{},
},
"interface reference containing nil fields is skipped": {
references: []*armcomputev2.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)
})
}
}

View file

@ -0,0 +1,115 @@
package azure
import (
"context"
"errors"
"fmt"
"regexp"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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
}
publicIPAddress, err := m.getScaleSetVMPublicIPAddress(ctx, resourceGroup, scaleSet, instanceID, networkInterfaces)
if err != nil {
return metadata.InstanceMetadata{}, err
}
return convertScaleSetVMToCoreInstance(scaleSet, vmResp.VirtualMachineScaleSetVM, networkInterfaces, publicIPAddress)
}
// 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.NewListPager(resourceGroup, nil)
for scaleSetPager.More() {
page, err := scaleSetPager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("retrieving scale sets: %w", err)
}
for _, scaleSet := range page.Value {
if scaleSet == nil || scaleSet.Name == nil {
continue
}
vmPager := m.virtualMachineScaleSetVMsAPI.NewListPager(resourceGroup, *scaleSet.Name, nil)
for vmPager.More() {
vmPage, err := vmPager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("retrieving vms: %w", err)
}
for _, vm := range vmPage.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, "")
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 armcomputev2.VirtualMachineScaleSetVM, networkInterfaces []armnetwork.Interface, publicIPAddress 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),
VPCIP: extractVPCIP(networkInterfaces),
PublicIP: publicIPAddress,
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
}

View file

@ -0,0 +1,328 @@
package azure
import (
"context"
"errors"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
armcomputev2 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"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",
VPCIP: "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",
VPCIP: "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: newScaleSetIMDSStub(),
networkInterfacesAPI: newNetworkInterfacesStub(),
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
scaleSetsAPI: newScaleSetsStub(),
wantInstances: wantInstances,
},
"invalid scale sets are skipped": {
imdsAPI: newScaleSetIMDSStub(),
networkInterfacesAPI: newNetworkInterfacesStub(),
virtualMachineScaleSetVMsAPI: newVirtualMachineScaleSetsVMsStub(),
scaleSetsAPI: newListContainingNilScaleSetStub(),
wantInstances: wantInstances,
},
"listVMs can return 0 VMs": {
imdsAPI: newScaleSetIMDSStub(),
networkInterfacesAPI: newNetworkInterfacesStub(),
virtualMachineScaleSetVMsAPI: &stubVirtualMachineScaleSetVMsAPI{pager: &stubVirtualMachineScaleSetVMPager{}},
scaleSetsAPI: newScaleSetsStub(),
wantInstances: []metadata.InstanceMetadata{},
},
"can skip nil in VM list": {
imdsAPI: newScaleSetIMDSStub(),
networkInterfacesAPI: newNetworkInterfacesStub(),
virtualMachineScaleSetVMsAPI: newListContainingNilScaleSetVirtualMachinesStub(),
scaleSetsAPI: newScaleSetsStub(),
wantInstances: wantInstances,
},
"converting instance fails": {
imdsAPI: newScaleSetIMDSStub(),
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 armcomputev2.VirtualMachineScaleSetVM
inInterface []armnetwork.Interface
inPublicIP string
wantErr bool
wantInstance metadata.InstanceMetadata
}{
"conversion works": {
inVM: armcomputev2.VirtualMachineScaleSetVM{
Name: to.Ptr("scale-set-name_instance-id"),
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
Tags: map[string]*string{"tag-key": to.Ptr("tag-value")},
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
OSProfile: &armcomputev2.OSProfile{
ComputerName: to.Ptr("scale-set-name-instance-id"),
},
},
},
inInterface: []armnetwork.Interface{
{
Name: to.Ptr("scale-set-name_instance-id"),
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/interface-name"),
Properties: &armnetwork.InterfacePropertiesFormat{
IPConfigurations: []*armnetwork.InterfaceIPConfiguration{
{
Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{
Primary: to.Ptr(true),
PrivateIPAddress: to.Ptr("192.0.2.0"),
},
},
},
},
},
},
inPublicIP: "192.0.2.100",
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",
VPCIP: "192.0.2.0",
PublicIP: "192.0.2.100",
SSHKeys: map[string][]string{},
},
},
"invalid instance": {
inVM: armcomputev2.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.inPublicIP)
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-controlplanes-abcd123",
wantRole: role.ControlPlane,
},
"node role": {
scaleSet: "constellation-scale-set-workers-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: armcomputev2.VirtualMachineScaleSetVM{},
}
}
func newListContainingNilScaleSetVirtualMachinesStub() *stubVirtualMachineScaleSetVMsAPI {
return &stubVirtualMachineScaleSetVMsAPI{
pager: &stubVirtualMachineScaleSetVMPager{
list: []armcomputev2.VirtualMachineScaleSetVM{
{
Name: to.Ptr("scale-set-name_instance-id"),
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id"),
InstanceID: to.Ptr("instance-id"),
Tags: map[string]*string{
"tag-key": to.Ptr("tag-value"),
},
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
NetworkProfile: &armcomputev2.NetworkProfile{
NetworkInterfaces: []*armcomputev2.NetworkInterfaceReference{
{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
},
},
},
OSProfile: &armcomputev2.OSProfile{
ComputerName: to.Ptr("scale-set-name-instance-id"),
LinuxConfiguration: &armcomputev2.LinuxConfiguration{
SSH: &armcomputev2.SSHConfiguration{
PublicKeys: []*armcomputev2.SSHPublicKey{
{
KeyData: to.Ptr("key-data"),
Path: to.Ptr("/home/user/.ssh/authorized_keys"),
},
},
},
},
},
},
},
},
},
}
}
func newListContainingInvalidScaleSetVirtualMachinesStub() *stubVirtualMachineScaleSetVMsAPI {
return &stubVirtualMachineScaleSetVMsAPI{
pager: &stubVirtualMachineScaleSetVMPager{
list: []armcomputev2.VirtualMachineScaleSetVM{
{
InstanceID: to.Ptr("instance-id"),
Properties: &armcomputev2.VirtualMachineScaleSetVMProperties{
OSProfile: &armcomputev2.OSProfile{},
NetworkProfile: &armcomputev2.NetworkProfile{
NetworkInterfaces: []*armcomputev2.NetworkInterfaceReference{
{
ID: to.Ptr("/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/scale-set-name/virtualMachines/instance-id/networkInterfaces/interface-name"),
},
},
},
},
},
},
},
}
}
func newListContainingNilScaleSetStub() *stubScaleSetsAPI {
return &stubScaleSetsAPI{
pager: &stubVirtualMachineScaleSetsClientListPager{
list: []armcomputev2.VirtualMachineScaleSet{{Name: to.Ptr("scale-set-name")}},
},
}
}

View file

@ -0,0 +1,23 @@
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.NewListPager(resourceGroup, nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("retrieving security groups: %w", err)
}
for _, securityGroup := range page.Value {
return securityGroup, nil
}
}
return nil, fmt.Errorf("no security group found for resource group %q", resourceGroup)
}

View file

@ -0,0 +1,25 @@
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.NewListPager(resourceGroup, nil)
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("retrieving virtual networks: %w", err)
}
for _, network := range page.Value {
if network != nil {
return network, nil
}
}
}
return nil, fmt.Errorf("no virtual network found in resource group %s", resourceGroup)
}

51
internal/cloud/gcp/api.go Normal file
View 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)
}

View file

@ -0,0 +1,59 @@
package gcp
import (
"github.com/edgelesssys/constellation/internal/kubernetes"
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) (kubernetes.Secrets, error) {
return kubernetes.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
}

View 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
internal/cloud/gcp/ccm.go Normal file
View file

@ -0,0 +1,164 @@
package gcp
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/edgelesssys/constellation/internal/cloud/metadata"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/edgelesssys/constellation/internal/kubernetes"
"github.com/edgelesssys/constellation/internal/versions"
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(k8sVersion versions.ValidK8sVersion) (string, error) {
return versions.VersionConfigs[k8sVersion].CloudControllerManagerImageGCP, nil
}
// 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) (kubernetes.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 kubernetes.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 kubernetes.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) (kubernetes.Secrets, error) {
serviceAccountKey, err := gcpshared.ServiceAccountKeyFromURI(cloudServiceAccountURI)
if err != nil {
return kubernetes.Secrets{}, err
}
rawKey, err := json.Marshal(serviceAccountKey)
if err != nil {
return kubernetes.Secrets{}, err
}
return kubernetes.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
}

View file

@ -0,0 +1,144 @@
package gcp
import (
"context"
"encoding/json"
"testing"
"github.com/edgelesssys/constellation/internal/cloud/metadata"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/edgelesssys/constellation/internal/kubernetes"
"github.com/edgelesssys/constellation/internal/versions"
"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 kubernetes.ConfigMaps
wantErr bool
}{
"ConfigMaps works": {
instance: metadata.InstanceMetadata{ProviderID: "gce://project-id/zone/instanceName-UID-0", Name: "instanceName-UID-0"},
wantConfigMaps: kubernetes.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 kubernetes.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: kubernetes.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(versions.Latest))
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())
}

View file

@ -0,0 +1,424 @@
package gcp
import (
"context"
"errors"
"fmt"
"net"
"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 || len(subnetwork.SecondaryIpRanges) == 0 || (subnetwork.SecondaryIpRanges[0]).IpCidrRange == nil {
return "", fmt.Errorf("retrieving subnetwork alias CIDR returned invalid results")
}
return *(subnetwork.SecondaryIpRanges[0]).IpCidrRange, nil
}
// RetrieveLoadBalancerEndpoint returns the endpoint of the load balancer with the constellation-uid tag.
func (c *Client) RetrieveLoadBalancerEndpoint(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{
Project: project,
Region: region,
}
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 {
if len(resp.Ports) == 0 {
return "", errors.New("load balancer with searched UID has no ports")
}
return net.JoinHostPort(*resp.IPAddress, resp.Ports[0]), 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
}
// extractVPCIP extracts the primary private IP from a list of interfaces.
func extractVPCIP(interfaces []*computepb.NetworkInterface) string {
for _, interf := range interfaces {
if interf == nil || interf.NetworkIP == nil || interf.Name == nil || *interf.Name != "nic0" {
continue
}
// return private IP from the default interface
return *interf.NetworkIP
}
return ""
}
// extractPublicIP extracts a public IP from a list of interfaces.
func extractPublicIP(interfaces []*computepb.NetworkInterface) string {
for _, interf := range interfaces {
if interf == nil || interf.AccessConfigs == nil || interf.Name == nil || *interf.Name != "nic0" {
continue
}
// return public IP from the default interface
// GCP only supports one type of access config, so returning the first IP should result in a valid public IP
for _, accessConfig := range interf.AccessConfigs {
if accessConfig == nil || accessConfig.NatIP == nil {
continue
}
return *accessConfig.NatIP
}
}
return ""
}
// 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),
VPCIP: extractVPCIP(in.NetworkInterfaces),
PublicIP: extractPublicIP(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
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
package gcp
import "github.com/edgelesssys/constellation/internal/versions"
// 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(k8sVersion versions.ValidK8sVersion) (string, error) {
return "", nil
}
// 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
}

View 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())
}

View 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()
}

View file

@ -0,0 +1,135 @@
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 {
// UID retrieves the current instances uid.
UID() (string, error)
// 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)
// RetrieveLoadBalancerEndpoint retrieves the load balancer endpoint of the current instance.
RetrieveLoadBalancerEndpoint(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
}
// GetLoadBalancerEndpoint returns the endpoint of the load balancer.
func (m *Metadata) GetLoadBalancerEndpoint(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.RetrieveLoadBalancerEndpoint(ctx, project, zone)
}
// UID retrieves the UID of the constellation.
func (m *Metadata) UID(ctx context.Context) (string, error) {
return m.api.UID()
}
// Supported is used to determine if metadata API is implemented for this cloud provider.
func (m *Metadata) Supported() bool {
return true
}

View file

@ -0,0 +1,319 @@
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",
VPCIP: "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",
VPCIP: "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",
VPCIP: "192.0.2.0",
},
},
wantInstance: metadata.InstanceMetadata{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
VPCIP: "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",
VPCIP: "192.0.2.0",
},
},
wantInstance: metadata.InstanceMetadata{
Name: "someInstance",
ProviderID: "gce://someProject/someZone/someInstance",
VPCIP: "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 {
retrieveUIDValue string
retrieveUIDErr error
retrieveInstanceValue metadata.InstanceMetadata
retrieveInstanceErr error
retrieveInstancesValues []metadata.InstanceMetadata
retrieveInstancesErr error
retrieveInstanceMetadaValues map[string]string
retrieveInstanceMetadataErr error
retrieveSubnetworkAliasErr 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) RetrieveLoadBalancerEndpoint(ctx context.Context, project, zone string) (string, error) {
return s.loadBalancerIP, s.retrieveLoadBalancerErr
}
func (s *stubGCPClient) UID() (string, error) {
return s.retrieveUIDValue, s.retrieveUIDErr
}
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.retrieveSubnetworkAliasErr
}

View 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
}
}

View file

@ -0,0 +1,47 @@
package gcp
import (
"testing"
"github.com/edgelesssys/constellation/bootstrapper/role"
"github.com/stretchr/testify/assert"
)
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)
})
}
}

View 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)
}

View 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
}

View 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))
})
}
}

View file

@ -0,0 +1,39 @@
package qemu
import (
"github.com/edgelesssys/constellation/internal/kubernetes"
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) (kubernetes.Secrets, error) {
return kubernetes.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
}

View file

@ -0,0 +1,73 @@
package qemu
import (
"context"
"github.com/edgelesssys/constellation/internal/cloud/metadata"
"github.com/edgelesssys/constellation/internal/kubernetes"
"github.com/edgelesssys/constellation/internal/versions"
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(k8sVersion versions.ValidK8sVersion) (string, error) {
return "", nil
}
// 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) (kubernetes.ConfigMaps, error) {
return kubernetes.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) (kubernetes.Secrets, error) {
return kubernetes.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
}

View file

@ -0,0 +1,29 @@
package qemu
import "github.com/edgelesssys/constellation/internal/versions"
// 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(k8sVersion versions.ValidK8sVersion) (string, error) {
return "", nil
}
// 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
}

View file

@ -0,0 +1,38 @@
package qemu
import (
"context"
"net/http"
"net/url"
"strings"
)
// Logger is a Cloud Logger for QEMU.
type Logger struct{}
// NewLogger creates a new Cloud Logger for QEMU.
func NewLogger() *Logger {
return &Logger{}
}
// Disclose writes log information to QEMU's cloud log.
// This is done by sending a POST request to the QEMU's metadata endpoint.
func (l *Logger) Disclose(msg string) {
url := &url.URL{
Scheme: "http",
Host: qemuMetadataEndpoint,
Path: "/log",
}
req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, url.String(), strings.NewReader(msg))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err == nil {
defer resp.Body.Close()
}
}
// Close is a no-op.
func (l *Logger) Close() error {
return nil
}

View file

@ -0,0 +1,103 @@
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
}
// GetLoadBalancerEndpoint returns the endpoint of the load balancer.
func (m Metadata) GetLoadBalancerEndpoint(ctx context.Context) (string, error) {
panic("function *Metadata.GetLoadBalancerEndpoint not implemented")
}
// UID returns the UID of the constellation.
func (m Metadata) UID(ctx context.Context) (string, error) {
// We expect only one constellation to be deployed in the same QEMU / libvirt environment.
// the UID can be an empty string.
return "", nil
}
// 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)
}