[node operator] self-initialize resources

Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Malte Poll 2022-07-29 15:00:15 +02:00 committed by Malte Poll
parent 1cee319174
commit 51cf638361
27 changed files with 1021 additions and 26 deletions

View file

@ -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

View file

@ -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},

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

@ -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,

View file

@ -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 {

View file

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