mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-06 05:54:28 -04:00
[node operator] self-initialize resources
Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
parent
1cee319174
commit
51cf638361
27 changed files with 1021 additions and 26 deletions
|
@ -3,6 +3,7 @@ package client
|
|||
import (
|
||||
"context"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
)
|
||||
|
@ -27,6 +28,8 @@ type instanceGroupManagersAPI interface {
|
|||
Close() error
|
||||
Get(ctx context.Context, req *computepb.GetInstanceGroupManagerRequest,
|
||||
opts ...gax.CallOption) (*computepb.InstanceGroupManager, error)
|
||||
AggregatedList(ctx context.Context, req *computepb.AggregatedListInstanceGroupManagersRequest,
|
||||
opts ...gax.CallOption) InstanceGroupManagerScopedListIterator
|
||||
SetInstanceTemplate(ctx context.Context, req *computepb.SetInstanceTemplateInstanceGroupManagerRequest,
|
||||
opts ...gax.CallOption) (Operation, error)
|
||||
CreateInstances(ctx context.Context, req *computepb.CreateInstancesInstanceGroupManagerRequest,
|
||||
|
@ -47,6 +50,10 @@ type Operation interface {
|
|||
Wait(ctx context.Context, opts ...gax.CallOption) error
|
||||
}
|
||||
|
||||
type InstanceGroupManagerScopedListIterator interface {
|
||||
Next() (compute.InstanceGroupManagersScopedListPair, error)
|
||||
}
|
||||
|
||||
type prng interface {
|
||||
// Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n). It panics if n <= 0.
|
||||
Intn(n int) int
|
||||
|
|
|
@ -6,11 +6,13 @@ import (
|
|||
"time"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
"github.com/spf13/afero"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
// Client is a client for the Google Compute Engine.
|
||||
type Client struct {
|
||||
projectID string
|
||||
instanceAPI
|
||||
instanceTemplateAPI
|
||||
instanceGroupManagersAPI
|
||||
|
@ -20,7 +22,12 @@ type Client struct {
|
|||
}
|
||||
|
||||
// New creates a new client for the Google Compute Engine.
|
||||
func New(ctx context.Context) (*Client, error) {
|
||||
func New(ctx context.Context, configPath string) (*Client, error) {
|
||||
projectID, err := loadProjectID(afero.NewOsFs(), configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var closers []closer
|
||||
insAPI, err := compute.NewInstancesRESTClient(ctx)
|
||||
if err != nil {
|
||||
|
@ -44,8 +51,8 @@ func New(ctx context.Context) (*Client, error) {
|
|||
_ = closeAll(closers)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
projectID: projectID,
|
||||
instanceAPI: insAPI,
|
||||
instanceTemplateAPI: &instanceTemplateClient{templAPI},
|
||||
instanceGroupManagersAPI: &instanceGroupManagersClient{groupAPI},
|
||||
|
|
|
@ -3,7 +3,9 @@ package client
|
|||
import (
|
||||
"context"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
"google.golang.org/api/iterator"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
@ -63,6 +65,7 @@ func (a stubInstanceTemplateAPI) Insert(ctx context.Context, req *computepb.Inse
|
|||
type stubInstanceGroupManagersAPI struct {
|
||||
instanceGroupManager *computepb.InstanceGroupManager
|
||||
getErr error
|
||||
aggregatedListErr error
|
||||
setInstanceTemplateErr error
|
||||
createInstancesErr error
|
||||
deleteInstancesErr error
|
||||
|
@ -78,6 +81,24 @@ func (a stubInstanceGroupManagersAPI) Get(ctx context.Context, req *computepb.Ge
|
|||
return a.instanceGroupManager, a.getErr
|
||||
}
|
||||
|
||||
func (a stubInstanceGroupManagersAPI) AggregatedList(ctx context.Context, req *computepb.AggregatedListInstanceGroupManagersRequest,
|
||||
opts ...gax.CallOption,
|
||||
) InstanceGroupManagerScopedListIterator {
|
||||
return &stubInstanceGroupManagerScopedListIterator{
|
||||
pairs: []compute.InstanceGroupManagersScopedListPair{
|
||||
{
|
||||
Key: "key",
|
||||
Value: &computepb.InstanceGroupManagersScopedList{
|
||||
InstanceGroupManagers: []*computepb.InstanceGroupManager{
|
||||
a.instanceGroupManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextErr: a.aggregatedListErr,
|
||||
}
|
||||
}
|
||||
|
||||
func (a stubInstanceGroupManagersAPI) SetInstanceTemplate(ctx context.Context, req *computepb.SetInstanceTemplateInstanceGroupManagerRequest,
|
||||
opts ...gax.CallOption,
|
||||
) (Operation, error) {
|
||||
|
@ -141,3 +162,22 @@ func (o *stubOperation) Done() bool {
|
|||
func (o *stubOperation) Wait(ctx context.Context, opts ...gax.CallOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type stubInstanceGroupManagerScopedListIterator struct {
|
||||
pairs []compute.InstanceGroupManagersScopedListPair
|
||||
nextErr error
|
||||
|
||||
internalCounter int
|
||||
}
|
||||
|
||||
func (i *stubInstanceGroupManagerScopedListIterator) Next() (compute.InstanceGroupManagersScopedListPair, error) {
|
||||
if i.nextErr != nil {
|
||||
return compute.InstanceGroupManagersScopedListPair{}, i.nextErr
|
||||
}
|
||||
if i.internalCounter >= len(i.pairs) {
|
||||
return compute.InstanceGroupManagersScopedListPair{}, iterator.Done
|
||||
}
|
||||
pair := i.pairs[i.internalCounter]
|
||||
i.internalCounter++
|
||||
return pair, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var projectIDRegex = regexp.MustCompile(`(?m)^project-id = (.*)$`)
|
||||
|
||||
// loadProjectID loads the project id from the gce config file.
|
||||
func loadProjectID(fs afero.Fs, path string) (string, error) {
|
||||
rawConfig, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// find project-id line
|
||||
matches := projectIDRegex.FindStringSubmatch(string(rawConfig))
|
||||
if len(matches) != 2 {
|
||||
return "", errors.New("invalid config: project-id not found")
|
||||
}
|
||||
return matches[1], nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadProjectID(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
rawConfig string
|
||||
skipWrite bool
|
||||
wantProjectID string
|
||||
wantErr bool
|
||||
}{
|
||||
"valid config": {
|
||||
rawConfig: `project-id = project-id`,
|
||||
wantProjectID: "project-id",
|
||||
},
|
||||
"invalid config": {
|
||||
rawConfig: `x = y`,
|
||||
wantErr: true,
|
||||
},
|
||||
"config is empty": {
|
||||
wantErr: true,
|
||||
},
|
||||
"config does not exist": {
|
||||
skipWrite: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
if !tc.skipWrite {
|
||||
require.NoError(afero.WriteFile(fs, "gce.conf", []byte(tc.rawConfig), 0o644))
|
||||
}
|
||||
gotProjectID, err := loadProjectID(fs, "gce.conf")
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantProjectID, gotProjectID)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -42,6 +42,12 @@ func (c *instanceGroupManagersClient) Get(ctx context.Context, req *computepb.Ge
|
|||
return c.InstanceGroupManagersClient.Get(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (c *instanceGroupManagersClient) AggregatedList(ctx context.Context, req *computepb.AggregatedListInstanceGroupManagersRequest,
|
||||
opts ...gax.CallOption,
|
||||
) InstanceGroupManagerScopedListIterator {
|
||||
return c.InstanceGroupManagersClient.AggregatedList(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (c *instanceGroupManagersClient) SetInstanceTemplate(ctx context.Context, req *computepb.SetInstanceTemplateInstanceGroupManagerRequest,
|
||||
opts ...gax.CallOption,
|
||||
) (Operation, error) {
|
||||
|
|
|
@ -5,7 +5,11 @@ import (
|
|||
"regexp"
|
||||
)
|
||||
|
||||
var instanceGroupIDRegex = regexp.MustCompile(`^projects/([^/]+)/zones/([^/]+)/instanceGroupManagers/([^/]+)$`)
|
||||
var (
|
||||
instanceGroupIDRegex = regexp.MustCompile(`^projects/([^/]+)/zones/([^/]+)/instanceGroupManagers/([^/]+)$`)
|
||||
controlPlaneInstanceGroupNameRegex = regexp.MustCompile(`^(.*)control-plane(.*)$`)
|
||||
workerInstanceGroupNameRegex = regexp.MustCompile(`^(.*)worker(.*)$`)
|
||||
)
|
||||
|
||||
// splitInstanceGroupID splits an instance group ID into core components.
|
||||
func splitInstanceGroupID(instanceGroupID string) (project, zone, instanceGroup string, err error) {
|
||||
|
@ -16,8 +20,18 @@ func splitInstanceGroupID(instanceGroupID string) (project, zone, instanceGroup
|
|||
return matches[1], matches[2], matches[3], nil
|
||||
}
|
||||
|
||||
// isControlPlaneInstanceGroup returns true if the instance group is a control plane instance group.
|
||||
func isControlPlaneInstanceGroup(instanceGroupName string) bool {
|
||||
return controlPlaneInstanceGroupNameRegex.MatchString(instanceGroupName)
|
||||
}
|
||||
|
||||
// isWorkerInstanceGroup returns true if the instance group is a worker instance group.
|
||||
func isWorkerInstanceGroup(instanceGroupName string) bool {
|
||||
return workerInstanceGroupNameRegex.MatchString(instanceGroupName)
|
||||
}
|
||||
|
||||
// generateInstanceName generates a random instance name.
|
||||
func generateInstanceName(baseInstanceName string, random prng) (string, error) {
|
||||
func generateInstanceName(baseInstanceName string, random prng) string {
|
||||
letters := []byte("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
const uidLen = 4
|
||||
uid := make([]byte, 0, uidLen)
|
||||
|
@ -25,5 +39,5 @@ func generateInstanceName(baseInstanceName string, random prng) (string, error)
|
|||
n := random.Intn(len(letters))
|
||||
uid = append(uid, letters[n])
|
||||
}
|
||||
return baseInstanceName + "-" + string(uid), nil
|
||||
return baseInstanceName + "-" + string(uid)
|
||||
}
|
||||
|
|
|
@ -49,21 +49,17 @@ func TestSplitInstanceGroupID(t *testing.T) {
|
|||
|
||||
func TestGenerateInstanceName(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
baseInstanceName := "base"
|
||||
gotInstanceName, err := generateInstanceName(baseInstanceName, &stubRng{result: 0})
|
||||
require.NoError(err)
|
||||
gotInstanceName := generateInstanceName(baseInstanceName, &stubRng{result: 0})
|
||||
assert.Equal("base-aaaa", gotInstanceName)
|
||||
}
|
||||
|
||||
func TestGenerateInstanceNameRandomTest(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
instanceNameRegexp := regexp.MustCompile(`^base-[0-9a-z]{4}$`)
|
||||
baseInstanceName := "base"
|
||||
random := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
gotInstanceName, err := generateInstanceName(baseInstanceName, random)
|
||||
require.NoError(err)
|
||||
gotInstanceName := generateInstanceName(baseInstanceName, random)
|
||||
assert.Regexp(instanceNameRegexp, gotInstanceName)
|
||||
}
|
||||
|
||||
|
|
|
@ -81,10 +81,7 @@ func (c *Client) CreateNode(ctx context.Context, scalingGroupID string) (nodeNam
|
|||
if instanceGroupManager.BaseInstanceName == nil {
|
||||
return "", "", fmt.Errorf("instance group manager %q has no base instance name", instanceGroupName)
|
||||
}
|
||||
instanceName, err := generateInstanceName(*instanceGroupManager.BaseInstanceName, c.prng)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
instanceName := generateInstanceName(*instanceGroupManager.BaseInstanceName, c.prng)
|
||||
op, err := c.instanceGroupManagersAPI.CreateInstances(ctx, &computepb.CreateInstancesInstanceGroupManagerRequest{
|
||||
InstanceGroupManager: instanceGroupName,
|
||||
Project: project,
|
||||
|
|
|
@ -4,8 +4,11 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/iterator"
|
||||
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// GetScalingGroupImage returns the image URI of the scaling group.
|
||||
|
@ -77,6 +80,47 @@ func (c *Client) SetScalingGroupImage(ctx context.Context, scalingGroupID, image
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetScalingGroupName retrieves the name of a scaling group.
|
||||
func (c *Client) GetScalingGroupName(ctx context.Context, scalingGroupID string) (string, error) {
|
||||
_, _, instanceGroupName, err := splitInstanceGroupID(scalingGroupID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting scaling group name: %w", err)
|
||||
}
|
||||
return strings.ToLower(instanceGroupName), nil
|
||||
}
|
||||
|
||||
// ListScalingGroups retrieves a list of scaling groups for the cluster.
|
||||
func (c *Client) ListScalingGroups(ctx context.Context, uid string) (controlPlaneGroupIDs []string, workerGroupIDs []string, err error) {
|
||||
iter := c.instanceGroupManagersAPI.AggregatedList(ctx, &computepb.AggregatedListInstanceGroupManagersRequest{
|
||||
Filter: proto.String(fmt.Sprintf("name eq \".+-.+-%s\"", uid)), // filter by constellation UID
|
||||
Project: c.projectID,
|
||||
})
|
||||
for instanceGroupManagerScopedListPair, err := iter.Next(); ; instanceGroupManagerScopedListPair, err = iter.Next() {
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("listing instance group managers: %w", err)
|
||||
}
|
||||
if instanceGroupManagerScopedListPair.Value == nil {
|
||||
continue
|
||||
}
|
||||
for _, instanceGroupManager := range instanceGroupManagerScopedListPair.Value.InstanceGroupManagers {
|
||||
if instanceGroupManager == nil || instanceGroupManager.Name == nil || instanceGroupManager.SelfLink == nil {
|
||||
continue
|
||||
}
|
||||
groupID := uriNormalize(*instanceGroupManager.SelfLink)
|
||||
|
||||
if isControlPlaneInstanceGroup(*instanceGroupManager.Name) {
|
||||
controlPlaneGroupIDs = append(controlPlaneGroupIDs, groupID)
|
||||
} else if isWorkerInstanceGroup(*instanceGroupManager.Name) {
|
||||
workerGroupIDs = append(workerGroupIDs, groupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return controlPlaneGroupIDs, workerGroupIDs, nil
|
||||
}
|
||||
|
||||
func (c *Client) getScalingGroupTemplate(ctx context.Context, scalingGroupID string) (*computepb.InstanceTemplate, error) {
|
||||
project, zone, instanceGroupName, err := splitInstanceGroupID(scalingGroupID)
|
||||
if err != nil {
|
||||
|
|
|
@ -282,3 +282,96 @@ func TestSetScalingGroupImage(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetScalingGroupName(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
scalingGroupID string
|
||||
wantName string
|
||||
wantErr bool
|
||||
}{
|
||||
"valid scaling group ID": {
|
||||
scalingGroupID: "projects/project/zones/zone/instanceGroupManagers/instance-group",
|
||||
wantName: "instance-group",
|
||||
},
|
||||
"invalid scaling group ID": {
|
||||
scalingGroupID: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{}
|
||||
gotName, err := client.GetScalingGroupName(context.Background(), tc.scalingGroupID)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantName, gotName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListScalingGroups(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
name *string
|
||||
groupID *string
|
||||
listInstanceGroupManagersErr error
|
||||
wantControlPlanes []string
|
||||
wantWorkers []string
|
||||
wantErr bool
|
||||
}{
|
||||
"list instance group managers fails": {
|
||||
listInstanceGroupManagersErr: errors.New("list instance group managers error"),
|
||||
wantErr: true,
|
||||
},
|
||||
"list instance group managers for control plane": {
|
||||
name: proto.String("test-control-plane-uid"),
|
||||
groupID: proto.String("projects/project/zones/zone/instanceGroupManagers/test-control-plane-uid"),
|
||||
wantControlPlanes: []string{
|
||||
"projects/project/zones/zone/instanceGroupManagers/test-control-plane-uid",
|
||||
},
|
||||
},
|
||||
"list instance group managers for worker": {
|
||||
name: proto.String("test-worker-uid"),
|
||||
groupID: proto.String("projects/project/zones/zone/instanceGroupManagers/test-worker-uid"),
|
||||
wantWorkers: []string{
|
||||
"projects/project/zones/zone/instanceGroupManagers/test-worker-uid",
|
||||
},
|
||||
},
|
||||
"unrelated instance group manager": {
|
||||
name: proto.String("test-unrelated-uid"),
|
||||
groupID: proto.String("projects/project/zones/zone/instanceGroupManagers/test-unrelated-uid"),
|
||||
},
|
||||
"invalid instance group manager": {},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{
|
||||
instanceGroupManagersAPI: &stubInstanceGroupManagersAPI{
|
||||
aggregatedListErr: tc.listInstanceGroupManagersErr,
|
||||
instanceGroupManager: &computepb.InstanceGroupManager{
|
||||
Name: tc.name,
|
||||
SelfLink: tc.groupID,
|
||||
},
|
||||
},
|
||||
}
|
||||
gotControlPlanes, gotWorkers, err := client.ListScalingGroups(context.Background(), "uid")
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.ElementsMatch(tc.wantControlPlanes, gotControlPlanes)
|
||||
assert.ElementsMatch(tc.wantWorkers, gotWorkers)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue