Skip to content

Commit

Permalink
hack: add cloud-init bootstrap via cluster object store
Browse files Browse the repository at this point in the history
  • Loading branch information
cbang-akamai committed Dec 12, 2024
1 parent b39a1df commit b7820ec
Show file tree
Hide file tree
Showing 16 changed files with 1,032 additions and 71 deletions.
20 changes: 20 additions & 0 deletions api/v1alpha2/linodecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ type LinodeClusterSpec struct {
// NodeBalancerFirewallRef is a reference to a NodeBalancer Firewall object. This makes the linode use the specified NodeBalancer Firewall.
NodeBalancerFirewallRef *corev1.ObjectReference `json:"nodeBalancerFirewallRef,omitempty"`

// ObjectStore defines a supporting Object Storage bucket for cluster operations. This is currently used for
// bootstrapping (e.g. Cloud-init).
// +optional
ObjectStore *ObjectStore `json:"objectStore,omitempty"`

// CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this cluster. If not
// supplied then the credentials of the controller will be used.
// +optional
Expand Down Expand Up @@ -173,6 +178,21 @@ type LinodeNBPortConfig struct {
NodeBalancerConfigID *int `json:"nodeBalancerConfigID,omitempty"`
}

// ObjectStore defines a supporting Object Storage bucket for cluster operations. This is currently used for
// bootstrapping (e.g. Cloud-init).
type ObjectStore struct {
// PresignedURLDuration defines the duration for which presigned URLs are valid.
//
// This is used to generate presigned URLs for S3 Bucket objects, which are used by
// control-plane and worker nodes to fetch bootstrap data.
//
// +optional
PresignedURLDuration *metav1.Duration `json:"presignedURLDuration,omitempty"`

// CredentialsRef is a reference to a Secret that contains the credentials to use for accessing the Cluster Object Store.
CredentialsRef corev1.SecretReference `json:"credentialsRef,omitempty"`
}

// +kubebuilder:object:root=true

// LinodeClusterList contains a list of LinodeCluster
Expand Down
27 changes: 27 additions & 0 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions cloud/scope/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ import (
"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/dns"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegrid"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/linode/linodego"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2"
"github.com/linode/cluster-api-provider-linode/observability/wrappers/linodeclient"
"github.com/linode/cluster-api-provider-linode/version"

Expand All @@ -33,6 +38,9 @@ const (

// MaxBodySize is the max payload size for Akamai edge dns client requests
maxBody = 131072

// defaultObjectStorageSignedUrlExpiry is the default expiration for Object Storage signed URls
defaultObjectStorageSignedUrlExpiry = 15 * time.Minute
)

type Option struct {
Expand Down Expand Up @@ -104,6 +112,47 @@ func CreateLinodeClient(config ClientConfig, opts ...Option) (LinodeClient, erro
), nil
}

func CreateS3Clients(ctx context.Context, crClient K8sClient, cluster infrav1alpha2.LinodeCluster) (S3Client, S3PresignClient, error) {
var (
configOpts = []func(*awsconfig.LoadOptions) error{
awsconfig.WithRegion("auto"),
}

clientOpts = []func(*s3.Options){}
)

// If we have a cluster object store bucket, get its configuration.
if cluster.Spec.ObjectStore != nil {
secret, err := getCredentials(ctx, crClient, cluster.Spec.ObjectStore.CredentialsRef, cluster.GetNamespace())
if err == nil {
var (
access_key = string(secret.Data["access_key"])
secret_key = string(secret.Data["secret_key"])
s3_endpoint = string(secret.Data["s3_endpoint"])
)

configOpts = append(configOpts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(access_key, secret_key, "")))
clientOpts = append(clientOpts, func(opts *s3.Options) {
opts.BaseEndpoint = aws.String(s3_endpoint)
})
}
}

config, err := awsconfig.LoadDefaultConfig(ctx, configOpts...)
if err != nil {
return nil, nil, fmt.Errorf("load s3 config: %w", err)
}

var (
s3Client = s3.NewFromConfig(config, clientOpts...)
s3PresignClient = s3.NewPresignClient(s3Client, func(opts *s3.PresignOptions) {
opts.Expires = defaultObjectStorageSignedUrlExpiry
})
)

return s3Client, s3PresignClient, nil
}

func setUpEdgeDNSInterface() (dnsInterface dns.DNS, err error) {
edgeRCConfig := edgegrid.Config{
Host: os.Getenv("AKAMAI_HOST"),
Expand Down
59 changes: 41 additions & 18 deletions cloud/scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ type MachineScopeParams struct {
}

type MachineScope struct {
Client K8sClient
PatchHelper *patch.Helper
Cluster *clusterv1.Cluster
Machine *clusterv1.Machine
TokenHash string
LinodeClient LinodeClient
LinodeCluster *infrav1alpha2.LinodeCluster
LinodeMachine *infrav1alpha2.LinodeMachine
Client K8sClient
S3Client S3Client
S3PresignClient S3PresignClient
PatchHelper *patch.Helper
Cluster *clusterv1.Cluster
Machine *clusterv1.Machine
TokenHash string
LinodeClient LinodeClient
LinodeCluster *infrav1alpha2.LinodeCluster
LinodeMachine *infrav1alpha2.LinodeMachine
}

func validateMachineScopeParams(params MachineScopeParams) error {
Expand Down Expand Up @@ -62,20 +64,28 @@ func NewMachineScope(ctx context.Context, linodeClientConfig ClientConfig, param
if err != nil {
return nil, fmt.Errorf("failed to create linode client: %w", err)
}

s3client, s3PresignClient, err := CreateS3Clients(ctx, params.Client, *params.LinodeCluster)
if err != nil {
return nil, fmt.Errorf("create s3 clients: %w", err)
}

helper, err := patch.NewHelper(params.LinodeMachine, params.Client)
if err != nil {
return nil, fmt.Errorf("failed to init patch helper: %w", err)
}

return &MachineScope{
Client: params.Client,
PatchHelper: helper,
Cluster: params.Cluster,
Machine: params.Machine,
TokenHash: GetHash(linodeClientConfig.Token),
LinodeClient: linodeClient,
LinodeCluster: params.LinodeCluster,
LinodeMachine: params.LinodeMachine,
Client: params.Client,
S3Client: s3client,
S3PresignClient: s3PresignClient,
PatchHelper: helper,
Cluster: params.Cluster,
Machine: params.Machine,
TokenHash: GetHash(linodeClientConfig.Token),
LinodeClient: linodeClient,
LinodeCluster: params.LinodeCluster,
LinodeMachine: params.LinodeMachine,
}, nil
}

Expand All @@ -102,7 +112,7 @@ func (s *MachineScope) AddFinalizer(ctx context.Context) error {
// GetBootstrapData returns the bootstrap data from the secret in the Machine's bootstrap.dataSecretName.
func (m *MachineScope) GetBootstrapData(ctx context.Context) ([]byte, error) {
if m.Machine.Spec.Bootstrap.DataSecretName == nil {
return []byte{}, fmt.Errorf(
return nil, fmt.Errorf(
"bootstrap data secret is nil for LinodeMachine %s/%s",
m.LinodeMachine.Namespace,
m.LinodeMachine.Name,
Expand All @@ -112,7 +122,7 @@ func (m *MachineScope) GetBootstrapData(ctx context.Context) ([]byte, error) {
secret := &corev1.Secret{}
key := types.NamespacedName{Namespace: m.LinodeMachine.Namespace, Name: *m.Machine.Spec.Bootstrap.DataSecretName}
if err := m.Client.Get(ctx, key, secret); err != nil {
return []byte{}, fmt.Errorf(
return nil, fmt.Errorf(
"failed to retrieve bootstrap data secret for LinodeMachine %s/%s",
m.LinodeMachine.Namespace,
m.LinodeMachine.Name,
Expand All @@ -131,6 +141,19 @@ func (m *MachineScope) GetBootstrapData(ctx context.Context) ([]byte, error) {
return value, nil
}

func (m *MachineScope) GetBucketName(ctx context.Context) (string, error) {
if m.LinodeCluster.Spec.ObjectStore == nil {
return "", errors.New("no cluster object store")
}

name, err := getCredentialDataFromRef(ctx, m.Client, m.LinodeCluster.Spec.ObjectStore.CredentialsRef, m.LinodeCluster.GetNamespace(), "bucket_name")
if err != nil {
return "", fmt.Errorf("get bucket name: %w", err)
}

return string(name), nil
}

func (s *MachineScope) AddCredentialsRefFinalizer(ctx context.Context) error {
// Only add the finalizer if the machine has an override for the credentials reference
if s.LinodeMachine.Spec.CredentialsRef == nil {
Expand Down
29 changes: 29 additions & 0 deletions cloud/scope/machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,35 @@ func TestNewMachineScope(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, mScope)
})),
Path(
Call("cluster object store used", func(ctx context.Context, mck Mock) {
mck.K8sClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj *corev1.Secret, opts ...client.GetOption) error {
secret := corev1.Secret{Data: map[string][]byte{
"bucket_name": []byte("fake"),
"s3_endpoint": []byte("fake"),
"access_key": []byte("fake"),
"secret_key": []byte("fake"),
}}
*obj = secret
return nil
})
}),
Result("success", func(ctx context.Context, mck Mock) {
mScope, err := NewMachineScope(ctx, ClientConfig{Token: "apiToken"}, MachineScopeParams{
Client: mck.K8sClient,
Cluster: &clusterv1.Cluster{},
Machine: &clusterv1.Machine{},
LinodeCluster: &infrav1alpha2.LinodeCluster{
Spec: infrav1alpha2.LinodeClusterSpec{
ObjectStore: &infrav1alpha2.ObjectStore{
CredentialsRef: corev1.SecretReference{Name: "fake"},
},
}},
LinodeMachine: &infrav1alpha2.LinodeMachine{},
})
require.NoError(t, err)
assert.NotNil(t, mScope)
})),
),
)
}
Expand Down
Loading

0 comments on commit b7820ec

Please sign in to comment.