mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 06:16:08 -04:00
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:
parent
89e3acf6a1
commit
26e9c67a00
81 changed files with 169 additions and 145 deletions
51
internal/cloud/gcp/api.go
Normal file
51
internal/cloud/gcp/api.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
)
|
||||
|
||||
type instanceAPI interface {
|
||||
Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error)
|
||||
List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) InstanceIterator
|
||||
SetMetadata(ctx context.Context, req *computepb.SetMetadataInstanceRequest, opts ...gax.CallOption) (*compute.Operation, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type subnetworkAPI interface {
|
||||
List(ctx context.Context, req *computepb.ListSubnetworksRequest, opts ...gax.CallOption) SubnetworkIterator
|
||||
Get(ctx context.Context, req *computepb.GetSubnetworkRequest, opts ...gax.CallOption) (*computepb.Subnetwork, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type forwardingRulesAPI interface {
|
||||
List(ctx context.Context, req *computepb.ListForwardingRulesRequest, opts ...gax.CallOption) ForwardingRuleIterator
|
||||
Close() error
|
||||
}
|
||||
|
||||
type metadataAPI interface {
|
||||
InstanceAttributeValue(attr string) (string, error)
|
||||
ProjectID() (string, error)
|
||||
Zone() (string, error)
|
||||
InstanceName() (string, error)
|
||||
}
|
||||
|
||||
type Operation interface {
|
||||
Proto() *computepb.Operation
|
||||
}
|
||||
|
||||
type InstanceIterator interface {
|
||||
Next() (*computepb.Instance, error)
|
||||
}
|
||||
|
||||
type SubnetworkIterator interface {
|
||||
Next() (*computepb.Subnetwork, error)
|
||||
}
|
||||
|
||||
type ForwardingRuleIterator interface {
|
||||
Next() (*computepb.ForwardingRule, error)
|
||||
}
|
59
internal/cloud/gcp/autoscaler.go
Normal file
59
internal/cloud/gcp/autoscaler.go
Normal 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
|
||||
}
|
19
internal/cloud/gcp/autoscaler_test.go
Normal file
19
internal/cloud/gcp/autoscaler_test.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrivialAutoscalerFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
autoscaler := Autoscaler{}
|
||||
|
||||
assert.NotEmpty(autoscaler.Name())
|
||||
assert.Empty(autoscaler.Secrets("", ""))
|
||||
assert.NotEmpty(autoscaler.Volumes())
|
||||
assert.NotEmpty(autoscaler.VolumeMounts())
|
||||
assert.NotEmpty(autoscaler.Env())
|
||||
assert.True(autoscaler.Supported())
|
||||
}
|
164
internal/cloud/gcp/ccm.go
Normal file
164
internal/cloud/gcp/ccm.go
Normal 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
|
||||
}
|
144
internal/cloud/gcp/ccm_test.go
Normal file
144
internal/cloud/gcp/ccm_test.go
Normal 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())
|
||||
}
|
424
internal/cloud/gcp/client.go
Normal file
424
internal/cloud/gcp/client.go
Normal 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
|
||||
}
|
1068
internal/cloud/gcp/client_test.go
Normal file
1068
internal/cloud/gcp/client_test.go
Normal file
File diff suppressed because it is too large
Load diff
29
internal/cloud/gcp/cloudnodemanager.go
Normal file
29
internal/cloud/gcp/cloudnodemanager.go
Normal 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
|
||||
}
|
17
internal/cloud/gcp/cloudnodemanager_test.go
Normal file
17
internal/cloud/gcp/cloudnodemanager_test.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrivialCNMFunctions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
cloud := CloudNodeManager{}
|
||||
|
||||
assert.Empty(cloud.Image(""))
|
||||
assert.Empty(cloud.Path())
|
||||
assert.Empty(cloud.ExtraArgs())
|
||||
assert.False(cloud.Supported())
|
||||
}
|
45
internal/cloud/gcp/logger.go
Normal file
45
internal/cloud/gcp/logger.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"cloud.google.com/go/logging"
|
||||
"github.com/edgelesssys/constellation/internal/gcpshared"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
client *logging.Client
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewLogger creates a new Cloud Logger for GCP.
|
||||
// https://cloud.google.com/logging/docs/setup/go
|
||||
func NewLogger(ctx context.Context, providerID string, logName string) (*Logger, error) {
|
||||
projectID, _, _, err := gcpshared.SplitProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := logging.NewClient(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := client.Logger(logName).StandardLogger(logging.Info)
|
||||
|
||||
return &Logger{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Disclose stores log information in GCP Cloud Logging! Do **NOT** log sensitive
|
||||
// information!
|
||||
func (l *Logger) Disclose(msg string) {
|
||||
l.logger.Println(msg)
|
||||
}
|
||||
|
||||
// Close waits for all buffer to be written.
|
||||
func (l *Logger) Close() error {
|
||||
return l.client.Close()
|
||||
}
|
135
internal/cloud/gcp/metadata.go
Normal file
135
internal/cloud/gcp/metadata.go
Normal 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
|
||||
}
|
319
internal/cloud/gcp/metadata_test.go
Normal file
319
internal/cloud/gcp/metadata_test.go
Normal 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
|
||||
}
|
19
internal/cloud/gcp/role.go
Normal file
19
internal/cloud/gcp/role.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"github.com/edgelesssys/constellation/bootstrapper/role"
|
||||
)
|
||||
|
||||
const roleMetadataKey = "constellation-role"
|
||||
|
||||
// extractRole extracts role from cloud provider metadata.
|
||||
func extractRole(metadata map[string]string) role.Role {
|
||||
switch metadata[roleMetadataKey] {
|
||||
case role.ControlPlane.String():
|
||||
return role.ControlPlane
|
||||
case role.Worker.String():
|
||||
return role.Worker
|
||||
default:
|
||||
return role.Unknown
|
||||
}
|
||||
}
|
47
internal/cloud/gcp/role_test.go
Normal file
47
internal/cloud/gcp/role_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
80
internal/cloud/gcp/wrappers.go
Normal file
80
internal/cloud/gcp/wrappers.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
)
|
||||
|
||||
type instanceClient struct {
|
||||
*compute.InstancesClient
|
||||
}
|
||||
|
||||
func (c *instanceClient) Close() error {
|
||||
return c.InstancesClient.Close()
|
||||
}
|
||||
|
||||
func (c *instanceClient) List(ctx context.Context, req *computepb.ListInstancesRequest,
|
||||
opts ...gax.CallOption,
|
||||
) InstanceIterator {
|
||||
return c.InstancesClient.List(ctx, req)
|
||||
}
|
||||
|
||||
type subnetworkClient struct {
|
||||
*compute.SubnetworksClient
|
||||
}
|
||||
|
||||
func (c *subnetworkClient) Close() error {
|
||||
return c.SubnetworksClient.Close()
|
||||
}
|
||||
|
||||
func (c *subnetworkClient) List(ctx context.Context, req *computepb.ListSubnetworksRequest,
|
||||
opts ...gax.CallOption,
|
||||
) SubnetworkIterator {
|
||||
return c.SubnetworksClient.List(ctx, req)
|
||||
}
|
||||
|
||||
func (c *subnetworkClient) Get(ctx context.Context, req *computepb.GetSubnetworkRequest,
|
||||
opts ...gax.CallOption,
|
||||
) (*computepb.Subnetwork, error) {
|
||||
return c.SubnetworksClient.Get(ctx, req)
|
||||
}
|
||||
|
||||
type forwardingRulesClient struct {
|
||||
*compute.ForwardingRulesClient
|
||||
}
|
||||
|
||||
func (c *forwardingRulesClient) Close() error {
|
||||
return c.ForwardingRulesClient.Close()
|
||||
}
|
||||
|
||||
func (c *forwardingRulesClient) List(ctx context.Context, req *computepb.ListForwardingRulesRequest,
|
||||
opts ...gax.CallOption,
|
||||
) ForwardingRuleIterator {
|
||||
return c.ForwardingRulesClient.List(ctx, req)
|
||||
}
|
||||
|
||||
type metadataClient struct{}
|
||||
|
||||
func (c *metadataClient) InstanceAttributeValue(attr string) (string, error) {
|
||||
return metadata.InstanceAttributeValue(attr)
|
||||
}
|
||||
|
||||
func (c *metadataClient) ProjectID() (string, error) {
|
||||
return metadata.ProjectID()
|
||||
}
|
||||
|
||||
func (c *metadataClient) Zone() (string, error) {
|
||||
return metadata.Zone()
|
||||
}
|
||||
|
||||
func (c *metadataClient) InstanceName() (string, error) {
|
||||
return metadata.InstanceName()
|
||||
}
|
||||
|
||||
func (c *metadataClient) ProjectAttributeValue(attr string) (string, error) {
|
||||
return metadata.ProjectAttributeValue(attr)
|
||||
}
|
20
internal/cloud/gcp/writer.go
Normal file
20
internal/cloud/gcp/writer.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Writer implements ConfigWriter.
|
||||
type Writer struct {
|
||||
fs afero.Afero
|
||||
}
|
||||
|
||||
// WriteGCEConf persists the GCE config on disk.
|
||||
func (w *Writer) WriteGCEConf(config string) error {
|
||||
if err := w.fs.WriteFile("/etc/gce.conf", []byte(config), 0o644); err != nil {
|
||||
return fmt.Errorf("writing gce config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
54
internal/cloud/gcp/writer_test.go
Normal file
54
internal/cloud/gcp/writer_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package gcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteGCEConf(t *testing.T) {
|
||||
config := "someConfig"
|
||||
|
||||
testCases := map[string]struct {
|
||||
fs afero.Afero
|
||||
wantValue string
|
||||
wantErr bool
|
||||
}{
|
||||
"write works": {
|
||||
fs: afero.Afero{
|
||||
Fs: afero.NewMemMapFs(),
|
||||
},
|
||||
wantValue: config,
|
||||
wantErr: false,
|
||||
},
|
||||
"write fails": {
|
||||
fs: afero.Afero{
|
||||
Fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
writer := Writer{
|
||||
fs: tc.fs,
|
||||
}
|
||||
err := writer.WriteGCEConf(config)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
value, err := tc.fs.ReadFile("/etc/gce.conf")
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantValue, string(value))
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue