Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multi-tenancy #142

Merged
merged 5 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v1alpha1/linodecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ type LinodeClusterSpec struct {
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +optional
VPCRef *corev1.ObjectReference `json:"vpcRef,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
CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"`
cbzzz marked this conversation as resolved.
Show resolved Hide resolved
}

// LinodeClusterStatus defines the observed state of LinodeCluster
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/linodevpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
)
Expand All @@ -41,6 +42,11 @@ type LinodeVPCSpec struct {
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +optional
Subnets []VPCSubnetCreateOptions `json:"subnets,omitempty"`

// CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this VPC. If not
// supplied then the credentials of the controller will be used.
// +optional
CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"`
}

// VPCSubnetCreateOptions defines subnet options
Expand Down
10 changes: 10 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

10 changes: 9 additions & 1 deletion cloud/scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,19 @@ func validateClusterScopeParams(params ClusterScopeParams) error {

// NewClusterScope creates a new Scope from the supplied parameters.
// This is meant to be called for each reconcile iteration.
func NewClusterScope(apiKey string, params ClusterScopeParams) (*ClusterScope, error) {
func NewClusterScope(ctx context.Context, apiKey string, params ClusterScopeParams) (*ClusterScope, error) {
if err := validateClusterScopeParams(params); err != nil {
return nil, err
}

// Override the controller credentials with ones from the Cluster's Secret reference (if supplied).
if params.LinodeCluster.Spec.CredentialsRef != nil {
data, err := getCredentialDataFromRef(ctx, params.Client, *params.LinodeCluster.Spec.CredentialsRef, params.LinodeCluster.GetNamespace())
if err != nil {
return nil, fmt.Errorf("credentials from secret ref: %w", err)
}
apiKey = string(data)
}
linodeClient := createLinodeClient(apiKey)

helper, err := patch.NewHelper(params.LinodeCluster, params.Client)
Expand Down
14 changes: 9 additions & 5 deletions cloud/scope/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,24 @@ func createLinodeClient(apiKey string) *linodego.Client {
return &linodeClient
}

func getCredentialDataFromRef(ctx context.Context, crClient client.Client, credentialsRef *corev1.SecretReference) ([]byte, error) {
secretRefName := client.ObjectKey{
func getCredentialDataFromRef(ctx context.Context, crClient client.Client, credentialsRef corev1.SecretReference, defaultNamespace string) ([]byte, error) {
secretRef := client.ObjectKey{
Name: credentialsRef.Name,
Namespace: credentialsRef.Namespace,
}
if secretRef.Namespace == "" {
secretRef.Namespace = defaultNamespace
}

var credSecret corev1.Secret
if err := crClient.Get(ctx, secretRefName, &credSecret); err != nil {
return nil, fmt.Errorf("failed to retrieve configured credentials secret %s: %w", secretRefName.String(), err)
if err := crClient.Get(ctx, secretRef, &credSecret); err != nil {
return nil, fmt.Errorf("get credentials secret %s/%s: %w", secretRef.Namespace, secretRef.Name, err)
}

// TODO: This key is hard-coded (for now) to match the externally-managed `manager-credentials` Secret.
rawData, ok := credSecret.Data["apiToken"]
cbzzz marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return nil, fmt.Errorf("credentials secret %s is missing an apiToken key", secretRefName.String())
return nil, fmt.Errorf("no apiToken key in credentials secret %s/%s", secretRef.Namespace, secretRef.Name)
}

return rawData, nil
Expand Down
10 changes: 9 additions & 1 deletion cloud/scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,19 @@ func validateMachineScopeParams(params MachineScopeParams) error {
return nil
}

func NewMachineScope(apiKey string, params MachineScopeParams) (*MachineScope, error) {
func NewMachineScope(ctx context.Context, apiKey string, params MachineScopeParams) (*MachineScope, error) {
if err := validateMachineScopeParams(params); err != nil {
return nil, err
}

// Override the controller credentials with ones from the Cluster's Secret reference (if supplied).
if params.LinodeCluster.Spec.CredentialsRef != nil {
data, err := getCredentialDataFromRef(ctx, params.Client, *params.LinodeCluster.Spec.CredentialsRef, params.LinodeCluster.GetNamespace())
if err != nil {
return nil, fmt.Errorf("credentials from cluster secret ref: %w", err)
}
apiKey = string(data)
}
linodeClient := createLinodeClient(apiKey)

helper, err := patch.NewHelper(params.LinodeMachine, params.Client)
Expand Down
6 changes: 1 addition & 5 deletions cloud/scope/object_storage_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ func NewObjectStorageBucketScope(ctx context.Context, apiKey string, params Obje

// Override the controller credentials with ones from the Cluster's Secret reference (if supplied).
if params.Object.Spec.CredentialsRef != nil {
credRef := *params.Object.Spec.CredentialsRef
if credRef.Namespace == "" {
credRef.Namespace = params.Object.Namespace
}
data, err := getCredentialDataFromRef(ctx, params.Client, &credRef)
data, err := getCredentialDataFromRef(ctx, params.Client, *params.Object.Spec.CredentialsRef, params.Object.GetNamespace())
if err != nil {
return nil, fmt.Errorf("credentials from cluster secret ref: %w", err)
}
Expand Down
10 changes: 9 additions & 1 deletion cloud/scope/vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,19 @@ func validateVPCScopeParams(params VPCScopeParams) error {

// NewVPCScope creates a new Scope from the supplied parameters.
// This is meant to be called for each reconcile iteration.
func NewVPCScope(apiKey string, params VPCScopeParams) (*VPCScope, error) {
func NewVPCScope(ctx context.Context, apiKey string, params VPCScopeParams) (*VPCScope, error) {
if err := validateVPCScopeParams(params); err != nil {
return nil, err
}

// Override the controller credentials with ones from the VPC's Secret reference (if supplied).
if params.LinodeVPC.Spec.CredentialsRef != nil {
eljohnson92 marked this conversation as resolved.
Show resolved Hide resolved
data, err := getCredentialDataFromRef(ctx, params.Client, *params.LinodeVPC.Spec.CredentialsRef, params.LinodeVPC.GetNamespace())
if err != nil {
return nil, fmt.Errorf("credentials from secret ref: %w", err)
}
apiKey = string(data)
}
linodeClient := createLinodeClient(apiKey)

helper, err := patch.NewHelper(params.LinodeVPC, params.Client)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ spec:
- host
- port
type: object
credentialsRef:
description: |-
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.
properties:
name:
description: name is unique within a namespace to reference a
secret resource.
type: string
namespace:
description: namespace defines the space within which the secret
name must be unique.
type: string
type: object
x-kubernetes-map-type: atomic
network:
description: NetworkSpec encapsulates all things related to Linode
network.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ spec:
- host
- port
type: object
credentialsRef:
description: |-
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.
properties:
name:
description: name is unique within a namespace to reference
a secret resource.
type: string
namespace:
description: namespace defines the space within which
the secret name must be unique.
type: string
type: object
x-kubernetes-map-type: atomic
network:
description: NetworkSpec encapsulates all things related to
Linode network.
Expand Down
15 changes: 15 additions & 0 deletions config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ spec:
spec:
description: LinodeVPCSpec defines the desired state of LinodeVPC
properties:
credentialsRef:
description: |-
CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this VPC. If not
supplied then the credentials of the controller will be used.
properties:
name:
description: name is unique within a namespace to reference a
secret resource.
type: string
namespace:
description: namespace defines the space within which the secret
name must be unique.
type: string
type: object
x-kubernetes-map-type: atomic
description:
type: string
label:
Expand Down
1 change: 1 addition & 0 deletions controller/linodecluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func (r *LinodeClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reques
}
// Create the cluster scope.
clusterScope, err := scope.NewClusterScope(
ctx,
r.LinodeApiKey,
scope.ClusterScopeParams{
Client: r.Client,
Expand Down
2 changes: 2 additions & 0 deletions controller/linodemachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques
linodeCluster := &infrav1alpha1.LinodeCluster{}

machineScope, err := scope.NewMachineScope(
ctx,
r.LinodeApiKey,
scope.MachineScopeParams{
Client: r.Client,
Expand All @@ -184,6 +185,7 @@ func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques
}

clusterScope, err := scope.NewClusterScope(
ctx,
r.LinodeApiKey,
scope.ClusterScopeParams{
Client: r.Client,
Expand Down
1 change: 1 addition & 0 deletions controller/linodevpc_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (r *LinodeVPCReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}

vpcScope, err := scope.NewVPCScope(
ctx,
r.LinodeApiKey,
scope.VPCScopeParams{
Client: r.Client,
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [k3s](./topics/flavors/k3s.md)
- [rke2](./topics/flavors/rke2.md)
- [Etcd](./topics/etcd.md)
- [Multi-Tenancy](./topics/multi-tenancy.md)
- [Development](./developers/development.md)
- [Releasing](./developers/releasing.md)
- [Reference](./reference/reference.md)
5 changes: 3 additions & 2 deletions docs/src/developers/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,13 @@ providers:
Here is a list of required configuration parameters:

```sh
# Cluster settings
## Cluster settings
export CLUSTER_NAME=capl-cluster
export KUBERNETES_VERSION=v1.29.1

# Linode settings
## Linode settings
export LINODE_REGION=us-ord
# Multi-tenancy: This may be changed for each cluster to deploy to different Linode accounts.
export LINODE_TOKEN=<your linode PAT>
export LINODE_CONTROL_PLANE_MACHINE_TYPE=g6-standard-2
export LINODE_MACHINE_TYPE=g6-standard-2
Expand Down
51 changes: 51 additions & 0 deletions docs/src/topics/multi-tenancy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Multi-Tenancy

CAPL can manage multi-tenant workload clusters across Linode accounts. Custom resources may reference an optional Secret
containing their Linode credentials (i.e. API token) to be used for the deployment of Linode resources (e.g. Linodes,
VPCs, NodeBalancers, etc.) associated with the cluster.

The following example shows a basic credentials Secret:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: linode-credentials
stringData:
apiToken: <LINODE_TOKEN>
```

```admonish warning
The Linode API token data must be put in a key named `apiToken`!
```

Which may be optionally consumed by one or more custom resource objects:

```yaml
# Example: LinodeCluster
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: LinodeCluster
metadata:
name: test-cluster
spec:
credentialsRef:
name: linode-credentials
...
---
# Example: LinodeVPC
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: LinodeVPC
metadata:
name: test-vpc
spec:
credentialsRef:
name: linode-credentials
...
```

Secrets from other namespaces by additionally specifying an optional
`.spec.credentialsRef.namespace` value.

```admonish warning
If `.spec.credentialsRef` is set for a LinodeCluster, it should also be set for adjacent resources (e.g. LinodeVPC).
```
2 changes: 2 additions & 0 deletions templates/flavors/base/linodeCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ metadata:
name: ${CLUSTER_NAME}
spec:
region: ${LINODE_REGION}
credentialsRef:
name: ${CLUSTER_NAME}-credentials
7 changes: 7 additions & 0 deletions templates/flavors/base/secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Secret
metadata:
name: ${CLUSTER_NAME}-credentials
stringData:
apiToken: ${LINODE_TOKEN}