diff --git a/cloud/scope/cluster.go b/cloud/scope/cluster.go index 81d3528d..6f51330d 100644 --- a/cloud/scope/cluster.go +++ b/cloud/scope/cluster.go @@ -50,7 +50,7 @@ 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(ctx context.Context, apiKey string, params ClusterScopeParams) (*ClusterScope, error) { +func NewClusterScope(ctx context.Context, linodeClientConfig ClientConfig, params ClusterScopeParams) (*ClusterScope, error) { if err := validateClusterScopeParams(params); err != nil { return nil, err } @@ -62,9 +62,9 @@ func NewClusterScope(ctx context.Context, apiKey string, params ClusterScopePara if err != nil { return nil, fmt.Errorf("credentials from secret ref: %w", err) } - apiKey = string(apiToken) + linodeClientConfig.Token = string(apiToken) } - linodeClient, err := CreateLinodeClient(apiKey, defaultClientTimeout) + linodeClient, err := CreateLinodeClient(linodeClientConfig) if err != nil { return nil, fmt.Errorf("failed to create linode client: %w", err) } diff --git a/cloud/scope/cluster_test.go b/cloud/scope/cluster_test.go index 9b4b54f0..ef3195cd 100644 --- a/cloud/scope/cluster_test.go +++ b/cloud/scope/cluster_test.go @@ -157,7 +157,7 @@ func TestClusterScopeMethods(t *testing.T) { cScope, err := NewClusterScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, ClusterScopeParams{ Cluster: testcase.fields.Cluster, LinodeCluster: testcase.fields.LinodeCluster, @@ -297,7 +297,7 @@ func TestNewClusterScope(t *testing.T) { LinodeCluster: &infrav1alpha2.LinodeCluster{}, }, }, - expectedError: fmt.Errorf("failed to create linode client: missing Linode API key"), + expectedError: fmt.Errorf("failed to create linode client: token cannot be empty"), expects: func(mock *mock.MockK8sClient) {}, }, } @@ -316,7 +316,7 @@ func TestNewClusterScope(t *testing.T) { testcase.args.params.Client = mockK8sClient - got, err := NewClusterScope(context.Background(), testcase.args.apiKey, testcase.args.params) + got, err := NewClusterScope(context.Background(), ClientConfig{Token: testcase.args.apiKey}, testcase.args.params) if testcase.expectedError != nil { assert.ErrorContains(t, err, testcase.expectedError.Error()) @@ -411,7 +411,7 @@ func TestClusterAddCredentialsRefFinalizer(t *testing.T) { cScope, err := NewClusterScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, ClusterScopeParams{ Cluster: testcase.fields.Cluster, LinodeCluster: testcase.fields.LinodeCluster, @@ -512,7 +512,7 @@ func TestRemoveCredentialsRefFinalizer(t *testing.T) { cScope, err := NewClusterScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, ClusterScopeParams{ Cluster: testcase.fields.Cluster, LinodeCluster: testcase.fields.LinodeCluster, diff --git a/cloud/scope/common.go b/cloud/scope/common.go index d1cc998a..f86677ed 100644 --- a/cloud/scope/common.go +++ b/cloud/scope/common.go @@ -2,6 +2,8 @@ package scope import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "net/http" @@ -13,7 +15,6 @@ import ( "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegrid" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session" "github.com/linode/linodego" - "golang.org/x/oauth2" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -44,29 +45,59 @@ func WithRetryCount(c int) Option { } } -func CreateLinodeClient(apiKey string, timeout time.Duration, opts ...Option) (LinodeClient, error) { - if apiKey == "" { - return nil, errors.New("missing Linode API key") +type ClientConfig struct { + Token string + BaseUrl string + RootCertificatePath string + + Timeout time.Duration +} + +func CreateLinodeClient(config ClientConfig, opts ...Option) (LinodeClient, error) { + if config.Token == "" { + return nil, errors.New("token cannot be empty") } - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: apiKey}) + timeout := defaultClientTimeout + if config.Timeout != 0 { + timeout = config.Timeout + } - oauth2Client := &http.Client{ - Transport: &oauth2.Transport{ - Source: tokenSource, - }, + // Use system cert pool if root CA cert was not provided explicitly for this client. + // Works around linodego not using system certs if LINODE_CA is provided, + // which affects all clients spawned via linodego.NewClient + tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} + if config.RootCertificatePath == "" { + systemCertPool, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed to load system cert pool: %w", err) + } + tlsConfig.RootCAs = systemCertPool + } + + httpClient := &http.Client{ Timeout: timeout, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, } - linodeClient := linodego.NewClient(oauth2Client) - linodeClient.SetUserAgent(fmt.Sprintf("CAPL/%s", version.GetVersion())) + newClient := linodego.NewClient(httpClient) + newClient.SetToken(config.Token) + if config.RootCertificatePath != "" { + newClient.SetRootCertificate(config.RootCertificatePath) + } + if config.BaseUrl != "" { + newClient.SetBaseURL(config.BaseUrl) + } + newClient.SetUserAgent(fmt.Sprintf("CAPL/%s", version.GetVersion())) for _, opt := range opts { - opt.set(&linodeClient) + opt.set(&newClient) } return linodeclient.NewLinodeClientWithTracing( - &linodeClient, + &newClient, linodeclient.DefaultDecorator(), ), nil } diff --git a/cloud/scope/common_test.go b/cloud/scope/common_test.go index 2be47be0..ef4e5e2a 100644 --- a/cloud/scope/common_test.go +++ b/cloud/scope/common_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -22,19 +23,28 @@ func TestCreateLinodeClient(t *testing.T) { t.Parallel() tests := []struct { - name string - apiKey string - expectedErr error + name string + token string + baseUrl string + rootCertificatePath string + timeout time.Duration + expectedErr error }{ { - "Success - Valid API Key", + "Success - Valid API token", "test-key", + "", + "", + 0, nil, }, { - "Error - Empty API Key", + "Error - Empty API token", + "", + "", "", - errors.New("missing Linode API key"), + 0, + errors.New("token cannot be empty"), }, } @@ -42,9 +52,13 @@ func TestCreateLinodeClient(t *testing.T) { testCase := tt t.Run(testCase.name, func(t *testing.T) { t.Parallel() - - got, err := CreateLinodeClient(testCase.apiKey, defaultClientTimeout) - + clientConfig := ClientConfig{ + Token: testCase.token, + Timeout: testCase.timeout, + BaseUrl: testCase.baseUrl, + RootCertificatePath: testCase.rootCertificatePath, + } + got, err := CreateLinodeClient(clientConfig) if testCase.expectedErr != nil { assert.EqualError(t, err, testCase.expectedErr.Error()) } else { diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 2180a4a6..83c5b90c 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -53,7 +53,7 @@ func validateMachineScopeParams(params MachineScopeParams) error { return nil } -func NewMachineScope(ctx context.Context, apiKey, dnsKey string, params MachineScopeParams) (*MachineScope, error) { +func NewMachineScope(ctx context.Context, linodeClientConfig, dnsClientConfig ClientConfig, params MachineScopeParams) (*MachineScope, error) { if err := validateMachineScopeParams(params); err != nil { return nil, err } @@ -84,22 +84,22 @@ func NewMachineScope(ctx context.Context, apiKey, dnsKey string, params MachineS if err != nil { return nil, fmt.Errorf("credentials from secret ref: %w", err) } - apiKey = string(apiToken) + linodeClientConfig.Token = string(apiToken) dnsToken, err := getCredentialDataFromRef(ctx, params.Client, *credentialRef, defaultNamespace, "dnsToken") if err != nil || len(dnsToken) == 0 { dnsToken = apiToken } - dnsKey = string(dnsToken) + dnsClientConfig.Token = string(dnsToken) } - linodeClient, err := CreateLinodeClient(apiKey, defaultClientTimeout, + linodeClient, err := CreateLinodeClient(linodeClientConfig, WithRetryCount(0), ) if err != nil { return nil, fmt.Errorf("failed to create linode client: %w", err) } - linodeDomainsClient, err := CreateLinodeClient(dnsKey, defaultClientTimeout, + linodeDomainsClient, err := CreateLinodeClient(dnsClientConfig, WithRetryCount(0), ) if err != nil { diff --git a/cloud/scope/machine_test.go b/cloud/scope/machine_test.go index a7010f57..7e2fcf3d 100644 --- a/cloud/scope/machine_test.go +++ b/cloud/scope/machine_test.go @@ -130,17 +130,22 @@ func TestMachineScopeAddFinalizer(t *testing.T) { }) })), Path(Result("has finalizer", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{ - Client: mck.K8sClient, - Cluster: &clusterv1.Cluster{}, - Machine: &clusterv1.Machine{}, - LinodeCluster: &infrav1alpha2.LinodeCluster{}, - LinodeMachine: &infrav1alpha2.LinodeMachine{ - ObjectMeta: metav1.ObjectMeta{ - Finalizers: []string{infrav1alpha2.MachineFinalizer}, + mScope, err := NewMachineScope( + ctx, + ClientConfig{Token: "apiToken"}, + ClientConfig{Token: "dnsToken"}, + MachineScopeParams{ + Client: mck.K8sClient, + Cluster: &clusterv1.Cluster{}, + Machine: &clusterv1.Machine{}, + LinodeCluster: &infrav1alpha2.LinodeCluster{}, + LinodeMachine: &infrav1alpha2.LinodeMachine{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{infrav1alpha2.MachineFinalizer}, + }, }, }, - }) + ) require.NoError(t, err) assert.NoError(t, mScope.AddFinalizer(ctx)) require.Len(t, mScope.LinodeMachine.Finalizers, 1) @@ -153,13 +158,17 @@ func TestMachineScopeAddFinalizer(t *testing.T) { mck.K8sClient.EXPECT().Patch(ctx, gomock.Any(), gomock.Any()).Return(nil) }), Result("finalizer added", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{ - Client: mck.K8sClient, - Cluster: &clusterv1.Cluster{}, - Machine: &clusterv1.Machine{}, - LinodeCluster: &infrav1alpha2.LinodeCluster{}, - LinodeMachine: &infrav1alpha2.LinodeMachine{}, - }) + mScope, err := NewMachineScope( + ctx, + ClientConfig{Token: "apiToken"}, + ClientConfig{Token: "dnsToken"}, + MachineScopeParams{ + Client: mck.K8sClient, + Cluster: &clusterv1.Cluster{}, + Machine: &clusterv1.Machine{}, + LinodeCluster: &infrav1alpha2.LinodeCluster{}, + LinodeMachine: &infrav1alpha2.LinodeMachine{}, + }) require.NoError(t, err) assert.NoError(t, mScope.AddFinalizer(ctx)) require.Len(t, mScope.LinodeMachine.Finalizers, 1) @@ -171,13 +180,17 @@ func TestMachineScopeAddFinalizer(t *testing.T) { mck.K8sClient.EXPECT().Patch(ctx, gomock.Any(), gomock.Any()).Return(errors.New("fail")) }), Result("error", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{ - Client: mck.K8sClient, - Cluster: &clusterv1.Cluster{}, - Machine: &clusterv1.Machine{}, - LinodeCluster: &infrav1alpha2.LinodeCluster{}, - LinodeMachine: &infrav1alpha2.LinodeMachine{}, - }) + mScope, err := NewMachineScope( + ctx, + ClientConfig{Token: "apiToken"}, + ClientConfig{Token: "dnsToken"}, + MachineScopeParams{ + Client: mck.K8sClient, + Cluster: &clusterv1.Cluster{}, + Machine: &clusterv1.Machine{}, + LinodeCluster: &infrav1alpha2.LinodeCluster{}, + LinodeMachine: &infrav1alpha2.LinodeMachine{}, + }) require.NoError(t, err) assert.Error(t, mScope.AddFinalizer(ctx)) @@ -193,12 +206,17 @@ func TestNewMachineScope(t *testing.T) { NewSuite(t, mock.MockK8sClient{}).Run( OneOf( Path(Result("invalid params", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{}) + mScope, err := NewMachineScope( + ctx, + ClientConfig{Token: "apiToken"}, + ClientConfig{Token: "dnsToken"}, + MachineScopeParams{}, + ) require.ErrorContains(t, err, "is required") assert.Nil(t, mScope) })), Path(Result("no token", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "", "", MachineScopeParams{ + mScope, err := NewMachineScope(ctx, ClientConfig{Token: ""}, ClientConfig{Token: ""}, MachineScopeParams{ Client: mck.K8sClient, Cluster: &clusterv1.Cluster{}, Machine: &clusterv1.Machine{}, @@ -213,7 +231,7 @@ func TestNewMachineScope(t *testing.T) { mck.K8sClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).Return(apierrors.NewNotFound(schema.GroupResource{}, "example")) }), Result("error", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "", "", MachineScopeParams{ + mScope, err := NewMachineScope(ctx, ClientConfig{Token: ""}, ClientConfig{Token: ""}, MachineScopeParams{ Client: mck.K8sClient, Cluster: &clusterv1.Cluster{}, Machine: &clusterv1.Machine{}, @@ -245,7 +263,7 @@ func TestNewMachineScope(t *testing.T) { mck.K8sClient.EXPECT().Scheme().Return(runtime.NewScheme()) }), Result("cannot init patch helper", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{ + mScope, err := NewMachineScope(ctx, ClientConfig{Token: "apiToken"}, ClientConfig{Token: "dnsToken"}, MachineScopeParams{ Client: mck.K8sClient, Cluster: &clusterv1.Cluster{}, Machine: &clusterv1.Machine{}, @@ -270,7 +288,7 @@ func TestNewMachineScope(t *testing.T) { }).AnyTimes() })), Path(Result("default credentials", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{ + mScope, err := NewMachineScope(ctx, ClientConfig{Token: "apiToken"}, ClientConfig{Token: "dnsToken"}, MachineScopeParams{ Client: mck.K8sClient, Cluster: &clusterv1.Cluster{}, Machine: &clusterv1.Machine{}, @@ -283,7 +301,7 @@ func TestNewMachineScope(t *testing.T) { ), OneOf( Path(Result("credentials from LinodeMachine credentialsRef", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "", "", MachineScopeParams{ + mScope, err := NewMachineScope(ctx, ClientConfig{Token: ""}, ClientConfig{Token: ""}, MachineScopeParams{ Client: mck.K8sClient, Cluster: &clusterv1.Cluster{}, Machine: &clusterv1.Machine{}, @@ -301,7 +319,7 @@ func TestNewMachineScope(t *testing.T) { assert.NotNil(t, mScope) })), Path(Result("credentials from LinodeCluster credentialsRef", func(ctx context.Context, mck Mock) { - mScope, err := NewMachineScope(ctx, "apiToken", "dnsToken", MachineScopeParams{ + mScope, err := NewMachineScope(ctx, ClientConfig{Token: "apiToken"}, ClientConfig{Token: "dnsToken"}, MachineScopeParams{ Client: mck.K8sClient, Cluster: &clusterv1.Cluster{}, Machine: &clusterv1.Machine{}, @@ -468,8 +486,8 @@ func TestMachineAddCredentialsRefFinalizer(t *testing.T) { mScope, err := NewMachineScope( context.Background(), - "apiToken", - "dnsToken", + ClientConfig{Token: "apiToken"}, + ClientConfig{Token: "dnsToken"}, MachineScopeParams{ Client: mockK8sClient, Cluster: &clusterv1.Cluster{}, @@ -562,8 +580,8 @@ func TestMachineRemoveCredentialsRefFinalizer(t *testing.T) { mScope, err := NewMachineScope( context.Background(), - "apiToken", - "dnsToken", + ClientConfig{Token: "apiToken"}, + ClientConfig{Token: "dnsToken"}, MachineScopeParams{ Client: mockK8sClient, Cluster: &clusterv1.Cluster{}, diff --git a/cloud/scope/object_storage_bucket.go b/cloud/scope/object_storage_bucket.go index a444772c..0ba2517d 100644 --- a/cloud/scope/object_storage_bucket.go +++ b/cloud/scope/object_storage_bucket.go @@ -65,8 +65,8 @@ func validateObjectStorageBucketScopeParams(params ObjectStorageBucketScopeParam return nil } -//nolint:dupl // TODO: Remove fields related to key provisioning from the bucket resource. -func NewObjectStorageBucketScope(ctx context.Context, apiKey string, params ObjectStorageBucketScopeParams) (*ObjectStorageBucketScope, error) { +// TODO: Remove fields related to key provisioning from the bucket resource. +func NewObjectStorageBucketScope(ctx context.Context, linodeClientConfig ClientConfig, params ObjectStorageBucketScopeParams) (*ObjectStorageBucketScope, error) { if err := validateObjectStorageBucketScopeParams(params); err != nil { return nil, err } @@ -78,9 +78,10 @@ func NewObjectStorageBucketScope(ctx context.Context, apiKey string, params Obje if err != nil { return nil, fmt.Errorf("credentials from secret ref: %w", err) } - apiKey = string(apiToken) + linodeClientConfig.Token = string(apiToken) } - linodeClient, err := CreateLinodeClient(apiKey, clientTimeout) + linodeClientConfig.Timeout = clientTimeout + linodeClient, err := CreateLinodeClient(linodeClientConfig) if err != nil { return nil, fmt.Errorf("failed to create linode client: %w", err) } diff --git a/cloud/scope/object_storage_bucket_test.go b/cloud/scope/object_storage_bucket_test.go index 3c908ff3..e235a28a 100644 --- a/cloud/scope/object_storage_bucket_test.go +++ b/cloud/scope/object_storage_bucket_test.go @@ -197,7 +197,7 @@ func TestNewObjectStorageBucketScope(t *testing.T) { Logger: &logr.Logger{}, }, }, - expectedErr: fmt.Errorf("failed to create linode client: missing Linode API key"), + expectedErr: fmt.Errorf("failed to create linode client: token cannot be empty"), expects: func(mock *mock.MockK8sClient) {}, }, } @@ -215,7 +215,7 @@ func TestNewObjectStorageBucketScope(t *testing.T) { testcase.args.params.Client = mockK8sClient - got, err := NewObjectStorageBucketScope(context.Background(), testcase.args.apiKey, testcase.args.params) + got, err := NewObjectStorageBucketScope(context.Background(), ClientConfig{Token: testcase.args.apiKey}, testcase.args.params) if testcase.expectedErr != nil { assert.ErrorContains(t, err, testcase.expectedErr.Error()) @@ -275,7 +275,7 @@ func TestObjectStorageBucketScopeMethods(t *testing.T) { objScope, err := NewObjectStorageBucketScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, ObjectStorageBucketScopeParams{ Client: mockK8sClient, Bucket: testcase.Bucket, diff --git a/cloud/scope/object_storage_key.go b/cloud/scope/object_storage_key.go index 64ee4441..6158852b 100644 --- a/cloud/scope/object_storage_key.go +++ b/cloud/scope/object_storage_key.go @@ -45,8 +45,7 @@ func validateObjectStorageKeyScopeParams(params ObjectStorageKeyScopeParams) err return nil } -//nolint:dupl // Temporary duplicate until key provisioning is removed from the bucket resource. -func NewObjectStorageKeyScope(ctx context.Context, apiKey string, params ObjectStorageKeyScopeParams) (*ObjectStorageKeyScope, error) { +func NewObjectStorageKeyScope(ctx context.Context, linodeClientConfig ClientConfig, params ObjectStorageKeyScopeParams) (*ObjectStorageKeyScope, error) { if err := validateObjectStorageKeyScopeParams(params); err != nil { return nil, err } @@ -55,12 +54,13 @@ func NewObjectStorageKeyScope(ctx context.Context, apiKey string, params ObjectS if params.Key.Spec.CredentialsRef != nil { // TODO: This key is hard-coded (for now) to match the externally-managed `manager-credentials` Secret. apiToken, err := getCredentialDataFromRef(ctx, params.Client, *params.Key.Spec.CredentialsRef, params.Key.GetNamespace(), "apiToken") - if err != nil { + if err != nil || len(apiToken) == 0 { return nil, fmt.Errorf("credentials from secret ref: %w", err) } - apiKey = string(apiToken) + linodeClientConfig.Token = string(apiToken) } - linodeClient, err := CreateLinodeClient(apiKey, clientTimeout) + linodeClientConfig.Timeout = clientTimeout + linodeClient, err := CreateLinodeClient(linodeClientConfig) if err != nil { return nil, fmt.Errorf("failed to create linode client: %w", err) } diff --git a/cloud/scope/object_storage_key_test.go b/cloud/scope/object_storage_key_test.go index b384eb4f..12fedc5b 100644 --- a/cloud/scope/object_storage_key_test.go +++ b/cloud/scope/object_storage_key_test.go @@ -197,7 +197,7 @@ func TestNewObjectStorageKeyScope(t *testing.T) { Logger: &logr.Logger{}, }, }, - expectedErr: fmt.Errorf("failed to create linode client: missing Linode API key"), + expectedErr: fmt.Errorf("failed to create linode client: token cannot be empty"), expects: func(mock *mock.MockK8sClient) {}, }, } @@ -215,7 +215,7 @@ func TestNewObjectStorageKeyScope(t *testing.T) { testcase.args.params.Client = mockK8sClient - got, err := NewObjectStorageKeyScope(context.Background(), testcase.args.apiKey, testcase.args.params) + got, err := NewObjectStorageKeyScope(context.Background(), ClientConfig{Token: testcase.args.apiKey}, testcase.args.params) if testcase.expectedErr != nil { assert.ErrorContains(t, err, testcase.expectedErr.Error()) @@ -276,7 +276,7 @@ func TestObjectStrorageKeyAddFinalizer(t *testing.T) { keyScope, err := NewObjectStorageKeyScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, ObjectStorageKeyScopeParams{ Client: mockK8sClient, Key: testcase.Key, diff --git a/cloud/scope/placement_group.go b/cloud/scope/placement_group.go index 77aea0b4..79c9a83f 100644 --- a/cloud/scope/placement_group.go +++ b/cloud/scope/placement_group.go @@ -96,7 +96,7 @@ func (s *PlacementGroupScope) RemoveCredentialsRefFinalizer(ctx context.Context) // This is meant to be called for each reconcile iteration. // //nolint:dupl // This is pretty much the same as VPC, maybe a candidate to use generics later. -func NewPlacementGroupScope(ctx context.Context, apiKey string, params PlacementGroupScopeParams) (*PlacementGroupScope, error) { +func NewPlacementGroupScope(ctx context.Context, linodeClientConfig ClientConfig, params PlacementGroupScopeParams) (*PlacementGroupScope, error) { if err := validatePlacementGroupScope(params); err != nil { return nil, err } @@ -108,9 +108,10 @@ func NewPlacementGroupScope(ctx context.Context, apiKey string, params Placement if err != nil { return nil, fmt.Errorf("credentials from secret ref: %w", err) } - apiKey = string(apiToken) + linodeClientConfig.Token = string(apiToken) } - linodeClient, err := CreateLinodeClient(apiKey, defaultClientTimeout, + linodeClient, err := CreateLinodeClient( + linodeClientConfig, WithRetryCount(0), ) if err != nil { diff --git a/cloud/scope/placement_group_test.go b/cloud/scope/placement_group_test.go index 8c55844d..dc5e7e1a 100644 --- a/cloud/scope/placement_group_test.go +++ b/cloud/scope/placement_group_test.go @@ -165,7 +165,7 @@ func TestNewPlacementGroupScope(t *testing.T) { }, }, expects: func(mock *mock.MockK8sClient) {}, - expectedError: fmt.Errorf("failed to create linode client: missing Linode API key"), + expectedError: fmt.Errorf("failed to create linode client: token cannot be empty"), }, { name: "Error - Pass in valid args but get an error when creating a new patch helper", @@ -194,7 +194,7 @@ func TestNewPlacementGroupScope(t *testing.T) { testcase.args.params.Client = mockK8sClient - got, err := NewPlacementGroupScope(context.Background(), testcase.args.apiKey, testcase.args.params) + got, err := NewPlacementGroupScope(context.Background(), ClientConfig{Token: testcase.args.apiKey}, testcase.args.params) if testcase.expectedError != nil { assert.ErrorContains(t, err, testcase.expectedError.Error()) @@ -259,7 +259,7 @@ func TestPlacementGroupScopeMethods(t *testing.T) { pgScope, err := NewPlacementGroupScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, PlacementGroupScopeParams{ Client: mockK8sClient, LinodePlacementGroup: testcase.LinodePlacementGroup, @@ -353,7 +353,7 @@ func TestPlacementGroupAddCredentialsRefFinalizer(t *testing.T) { pgScope, err := NewPlacementGroupScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, PlacementGroupScopeParams{ Client: mockK8sClient, LinodePlacementGroup: testcase.LinodePlacementGroup, @@ -443,7 +443,7 @@ func TestPlacementGroupRemoveCredentialsRefFinalizer(t *testing.T) { pgScope, err := NewPlacementGroupScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, PlacementGroupScopeParams{ Client: mockK8sClient, LinodePlacementGroup: testcase.LinodePlacementGroup, diff --git a/cloud/scope/vpc.go b/cloud/scope/vpc.go index ce4ba866..e8731249 100644 --- a/cloud/scope/vpc.go +++ b/cloud/scope/vpc.go @@ -56,7 +56,7 @@ func validateVPCScopeParams(params VPCScopeParams) error { // This is meant to be called for each reconcile iteration. // //nolint:dupl // this is the same as PlacementGroups - worth making into generics later. -func NewVPCScope(ctx context.Context, apiKey string, params VPCScopeParams) (*VPCScope, error) { +func NewVPCScope(ctx context.Context, linodeClientConfig ClientConfig, params VPCScopeParams) (*VPCScope, error) { if err := validateVPCScopeParams(params); err != nil { return nil, err } @@ -68,9 +68,9 @@ func NewVPCScope(ctx context.Context, apiKey string, params VPCScopeParams) (*VP if err != nil { return nil, fmt.Errorf("credentials from secret ref: %w", err) } - apiKey = string(apiToken) + linodeClientConfig.Token = string(apiToken) } - linodeClient, err := CreateLinodeClient(apiKey, defaultClientTimeout, + linodeClient, err := CreateLinodeClient(linodeClientConfig, WithRetryCount(0), ) if err != nil { diff --git a/cloud/scope/vpc_test.go b/cloud/scope/vpc_test.go index a4c2cfd9..02e79d4a 100644 --- a/cloud/scope/vpc_test.go +++ b/cloud/scope/vpc_test.go @@ -165,7 +165,7 @@ func TestNewVPCScope(t *testing.T) { }, }, expects: func(mock *mock.MockK8sClient) {}, - expectedError: fmt.Errorf("failed to create linode client: missing Linode API key"), + expectedError: fmt.Errorf("failed to create linode client: token cannot be empty"), }, { name: "Error - Pass in valid args but get an error when creating a new patch helper", @@ -194,7 +194,7 @@ func TestNewVPCScope(t *testing.T) { testcase.args.params.Client = mockK8sClient - got, err := NewVPCScope(context.Background(), testcase.args.apiKey, testcase.args.params) + got, err := NewVPCScope(context.Background(), ClientConfig{Token: testcase.args.apiKey}, testcase.args.params) if testcase.expectedError != nil { assert.ErrorContains(t, err, testcase.expectedError.Error()) @@ -259,7 +259,7 @@ func TestVPCScopeMethods(t *testing.T) { vScope, err := NewVPCScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, VPCScopeParams{ Client: mockK8sClient, LinodeVPC: testcase.LinodeVPC, @@ -353,7 +353,7 @@ func TestVPCAddCredentialsRefFinalizer(t *testing.T) { vScope, err := NewVPCScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, VPCScopeParams{ Client: mockK8sClient, LinodeVPC: testcase.LinodeVPC, @@ -443,7 +443,7 @@ func TestVPCRemoveCredentialsRefFinalizer(t *testing.T) { vScope, err := NewVPCScope( context.Background(), - "test-key", + ClientConfig{Token: "test-key"}, VPCScopeParams{ Client: mockK8sClient, LinodeVPC: testcase.LinodeVPC, diff --git a/cmd/main.go b/cmd/main.go index ded7b3e8..7337bbd9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -41,6 +41,7 @@ import ( infrastructurev1alpha1 "github.com/linode/cluster-api-provider-linode/api/v1alpha1" infrastructurev1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2" + "github.com/linode/cluster-api-provider-linode/cloud/scope" "github.com/linode/cluster-api-provider-linode/controller" "github.com/linode/cluster-api-provider-linode/observability/tracing" "github.com/linode/cluster-api-provider-linode/version" @@ -80,6 +81,8 @@ func main() { // Environment variables linodeToken = os.Getenv("LINODE_TOKEN") linodeDNSToken = os.Getenv("LINODE_DNS_TOKEN") + linodeDNSURL = os.Getenv("LINODE_DNS_URL") + linodeDNSCA = os.Getenv("LINODE_DNS_CA") machineWatchFilter string clusterWatchFilter string @@ -133,9 +136,13 @@ func main() { os.Exit(1) } if linodeDNSToken == "" { + setupLog.Info("LINODE_DNS_TOKEN not provided, defaulting to the value of LINODE_TOKEN") linodeDNSToken = linodeToken } + linodeClientConfig := scope.ClientConfig{Token: linodeToken} + dnsClientConfig := scope.ClientConfig{Token: linodeDNSToken, BaseUrl: linodeDNSURL, RootCertificatePath: linodeDNSCA} + restConfig := ctrl.GetConfigOrDie() restConfig.QPS = float32(restConfigQPS) restConfig.Burst = restConfigBurst @@ -165,64 +172,64 @@ func main() { } if err = (&controller.LinodeClusterReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("LinodeClusterReconciler"), - WatchFilterValue: clusterWatchFilter, - LinodeApiKey: linodeToken, + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("LinodeClusterReconciler"), + WatchFilterValue: clusterWatchFilter, + LinodeClientConfig: linodeClientConfig, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: linodeClusterConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodeCluster") os.Exit(1) } if err = (&controller.LinodeMachineReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("LinodeMachineReconciler"), - WatchFilterValue: machineWatchFilter, - LinodeApiKey: linodeToken, - LinodeDNSAPIKey: linodeDNSToken, + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("LinodeMachineReconciler"), + WatchFilterValue: machineWatchFilter, + LinodeClientConfig: linodeClientConfig, + DnsClientConfig: dnsClientConfig, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: linodeMachineConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodeMachine") os.Exit(1) } if err = (&controller.LinodeVPCReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("LinodeVPCReconciler"), - WatchFilterValue: clusterWatchFilter, - LinodeApiKey: linodeToken, + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("LinodeVPCReconciler"), + WatchFilterValue: clusterWatchFilter, + LinodeClientConfig: linodeClientConfig, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: linodeVPCConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodeVPC") os.Exit(1) } if err = (&controller.LinodeObjectStorageBucketReconciler{ - Client: mgr.GetClient(), - Logger: ctrl.Log.WithName("LinodeObjectStorageBucketReconciler"), - Recorder: mgr.GetEventRecorderFor("LinodeObjectStorageBucketReconciler"), - WatchFilterValue: objectStorageBucketWatchFilter, - LinodeApiKey: linodeToken, + Client: mgr.GetClient(), + Logger: ctrl.Log.WithName("LinodeObjectStorageBucketReconciler"), + Recorder: mgr.GetEventRecorderFor("LinodeObjectStorageBucketReconciler"), + WatchFilterValue: objectStorageBucketWatchFilter, + LinodeClientConfig: linodeClientConfig, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: linodeObjectStorageBucketConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodeObjectStorageBucket") os.Exit(1) } if err = (&controller.LinodePlacementGroupReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("LinodePlacementGroupReconciler"), - WatchFilterValue: clusterWatchFilter, - LinodeApiKey: linodeToken, + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("LinodePlacementGroupReconciler"), + WatchFilterValue: clusterWatchFilter, + LinodeClientConfig: linodeClientConfig, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: linodePlacementGroupConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodePlacementGroup") os.Exit(1) } if err = (&controller.LinodeObjectStorageKeyReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Logger: ctrl.Log.WithName("LinodeObjectStorageKeyReconciler"), - Recorder: mgr.GetEventRecorderFor("LinodeObjectStorageKeyReconciler"), - WatchFilterValue: objectStorageKeyWatchFilter, - LinodeApiKey: linodeToken, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Logger: ctrl.Log.WithName("LinodeObjectStorageKeyReconciler"), + Recorder: mgr.GetEventRecorderFor("LinodeObjectStorageKeyReconciler"), + WatchFilterValue: objectStorageKeyWatchFilter, + LinodeClientConfig: linodeClientConfig, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: linodeObjectStorageBucketConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodeObjectStorageKey") os.Exit(1) diff --git a/controller/linodecluster_controller.go b/controller/linodecluster_controller.go index dfe77f0a..50cf9752 100644 --- a/controller/linodecluster_controller.go +++ b/controller/linodecluster_controller.go @@ -52,10 +52,10 @@ import ( // LinodeClusterReconciler reconciles a LinodeCluster object type LinodeClusterReconciler struct { client.Client - Recorder record.EventRecorder - LinodeApiKey string - WatchFilterValue string - ReconcileTimeout time.Duration + Recorder record.EventRecorder + LinodeClientConfig scope.ClientConfig + WatchFilterValue string + ReconcileTimeout time.Duration } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodeclusters,verbs=get;list;watch;create;update;patch;delete @@ -91,7 +91,7 @@ func (r *LinodeClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Create the cluster scope. clusterScope, err := scope.NewClusterScope( ctx, - r.LinodeApiKey, + r.LinodeClientConfig, scope.ClusterScopeParams{ Client: r.TracedClient(), Cluster: cluster, diff --git a/controller/linodemachine_controller.go b/controller/linodemachine_controller.go index c2aabece..4a69cf42 100644 --- a/controller/linodemachine_controller.go +++ b/controller/linodemachine_controller.go @@ -91,11 +91,11 @@ var requeueInstanceStatuses = map[linodego.InstanceStatus]bool{ // LinodeMachineReconciler reconciles a LinodeMachine object type LinodeMachineReconciler struct { client.Client - Recorder record.EventRecorder - LinodeApiKey string - LinodeDNSAPIKey string - WatchFilterValue string - ReconcileTimeout time.Duration + Recorder record.EventRecorder + LinodeClientConfig scope.ClientConfig + DnsClientConfig scope.ClientConfig + WatchFilterValue string + ReconcileTimeout time.Duration } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=get;list;watch;create;update;patch;delete @@ -140,8 +140,8 @@ func (r *LinodeMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques machineScope, err := scope.NewMachineScope( ctx, - r.LinodeApiKey, - r.LinodeDNSAPIKey, + r.LinodeClientConfig, + r.DnsClientConfig, scope.MachineScopeParams{ Client: r.TracedClient(), Cluster: cluster, diff --git a/controller/linodeobjectstoragebucket_controller.go b/controller/linodeobjectstoragebucket_controller.go index 68d79be3..57387116 100644 --- a/controller/linodeobjectstoragebucket_controller.go +++ b/controller/linodeobjectstoragebucket_controller.go @@ -53,11 +53,11 @@ import ( // LinodeObjectStorageBucketReconciler reconciles a LinodeObjectStorageBucket object type LinodeObjectStorageBucketReconciler struct { client.Client - Logger logr.Logger - Recorder record.EventRecorder - LinodeApiKey string - WatchFilterValue string - ReconcileTimeout time.Duration + Logger logr.Logger + Recorder record.EventRecorder + LinodeClientConfig scope.ClientConfig + WatchFilterValue string + ReconcileTimeout time.Duration } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodeobjectstoragebuckets,verbs=get;list;watch;create;update;patch;delete @@ -93,7 +93,7 @@ func (r *LinodeObjectStorageBucketReconciler) Reconcile(ctx context.Context, req bScope, err := scope.NewObjectStorageBucketScope( ctx, - r.LinodeApiKey, + r.LinodeClientConfig, scope.ObjectStorageBucketScopeParams{ Client: r.TracedClient(), Bucket: objectStorageBucket, diff --git a/controller/linodeobjectstoragekey_controller.go b/controller/linodeobjectstoragekey_controller.go index 12afd5ce..6fbee7c3 100644 --- a/controller/linodeobjectstoragekey_controller.go +++ b/controller/linodeobjectstoragekey_controller.go @@ -54,12 +54,12 @@ import ( // LinodeObjectStorageKeyReconciler reconciles a LinodeObjectStorageKey object type LinodeObjectStorageKeyReconciler struct { client.Client - Logger logr.Logger - Recorder record.EventRecorder - LinodeApiKey string - WatchFilterValue string - Scheme *runtime.Scheme - ReconcileTimeout time.Duration + Logger logr.Logger + Recorder record.EventRecorder + LinodeClientConfig scope.ClientConfig + WatchFilterValue string + Scheme *runtime.Scheme + ReconcileTimeout time.Duration } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodeobjectstoragekeys,verbs=get;list;watch;create;update;patch;delete @@ -97,7 +97,7 @@ func (r *LinodeObjectStorageKeyReconciler) Reconcile(ctx context.Context, req ct keyScope, err := scope.NewObjectStorageKeyScope( ctx, - r.LinodeApiKey, + r.LinodeClientConfig, scope.ObjectStorageKeyScopeParams{ Client: tracedClient, Key: objectStorageKey, diff --git a/controller/linodeplacementgroup_controller.go b/controller/linodeplacementgroup_controller.go index 43e4f1d0..eedd63c3 100644 --- a/controller/linodeplacementgroup_controller.go +++ b/controller/linodeplacementgroup_controller.go @@ -54,11 +54,11 @@ import ( // LinodePlacementGroupReconciler reconciles a LinodePlacementGroup object type LinodePlacementGroupReconciler struct { client.Client - Recorder record.EventRecorder - LinodeApiKey string - WatchFilterValue string - Scheme *runtime.Scheme - ReconcileTimeout time.Duration + Recorder record.EventRecorder + LinodeClientConfig scope.ClientConfig + WatchFilterValue string + Scheme *runtime.Scheme + ReconcileTimeout time.Duration } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodeplacementgroups,verbs=get;list;watch;create;update;patch;delete @@ -88,7 +88,7 @@ func (r *LinodePlacementGroupReconciler) Reconcile(ctx context.Context, req ctrl pgScope, err := scope.NewPlacementGroupScope( ctx, - r.LinodeApiKey, + r.LinodeClientConfig, scope.PlacementGroupScopeParams{ Client: r.TracedClient(), LinodePlacementGroup: linodeplacementgroup, diff --git a/controller/linodevpc_controller.go b/controller/linodevpc_controller.go index f8ee801b..401b10bf 100644 --- a/controller/linodevpc_controller.go +++ b/controller/linodevpc_controller.go @@ -53,11 +53,11 @@ import ( // LinodeVPCReconciler reconciles a LinodeVPC object type LinodeVPCReconciler struct { client.Client - Recorder record.EventRecorder - LinodeApiKey string - WatchFilterValue string - Scheme *runtime.Scheme - ReconcileTimeout time.Duration + Recorder record.EventRecorder + LinodeClientConfig scope.ClientConfig + WatchFilterValue string + Scheme *runtime.Scheme + ReconcileTimeout time.Duration } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodevpcs,verbs=get;list;watch;create;update;patch;delete @@ -94,7 +94,7 @@ func (r *LinodeVPCReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( vpcScope, err := scope.NewVPCScope( ctx, - r.LinodeApiKey, + r.LinodeClientConfig, scope.VPCScopeParams{ Client: r.TracedClient(), LinodeVPC: linodeVPC, diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index b08265bb..2126b8be 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -16,6 +16,7 @@ - [rke2](./topics/flavors/rke2.md) - [vpcless](./topics/flavors/vpcless.md) - [konnectivity (kubeadm)](./topics/flavors/konnectivity.md) + - [DNS based apiserver Load Balancing](./topics/flavors/dns-loadbalancing.md) - [Etcd](./topics/etcd.md) - [Backups](./topics/backups.md) - [Multi-Tenancy](./topics/multi-tenancy.md) diff --git a/docs/src/topics/flavors/dns-loadbalancing.md b/docs/src/topics/flavors/dns-loadbalancing.md index d477241b..359e2404 100644 --- a/docs/src/topics/flavors/dns-loadbalancing.md +++ b/docs/src/topics/flavors/dns-loadbalancing.md @@ -26,7 +26,18 @@ With these changes, the controlPlaneEndpoint is set to `- The controller will create A/AAAA and TXT records under [the Domains tab in the Linode Cloud Manager.](https://cloud.linode.com/domains) or Akamai Edge DNS depending on the provider. ### Linode Domains: -Using the `LINODE_DNS_TOKEN` env var, you can pass the [API token of a different account](https://cloud.linode.com/profile/tokens) if the Domain has been created in another acount under Linode CM +Using the `LINODE_DNS_TOKEN` env var, you can pass the [API token of a different account](https://cloud.linode.com/profile/tokens) if the Domain has been created in another acount under Linode CM: + +```bash +export LINODE_DNS_TOKEN= +``` + +Optionally, provide an alternative Linode API URL and root CA certificate. + +```bash +export LINODE_DNS_URL=custom.api.linode.com +export LINODE_DNS_CA=/path/to/cacert.pem +``` ### Akamai Domains: For the controller to authenticate with the Edge DNS API, you'll need to set the following env vars when creating the mgmt cluster. diff --git a/docs/src/topics/getting-started.md b/docs/src/topics/getting-started.md index c8ceb635..38193195 100644 --- a/docs/src/topics/getting-started.md +++ b/docs/src/topics/getting-started.md @@ -28,6 +28,12 @@ export LINODE_TOKEN= export LINODE_CONTROL_PLANE_MACHINE_TYPE=g6-standard-2 export LINODE_MACHINE_TYPE=g6-standard-2 ``` + +```admonish info +This project uses [linodego](https://github.com/linode/linodego) for Linode API interaction. +Please refer to it for more details on environment variables used for client configuration. +``` + ```admonish warning For Regions and Images that do not yet support Akamai's cloud-init datasource CAPL will automatically use a stackscript shim to provision the node. If you are using a custom image ensure the [cloud_init](https://www.linode.com/docs/api/images/#image-create) flag is set correctly on it diff --git a/go.mod b/go.mod index c26dd9fc..adad802d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/mod v0.19.0 - golang.org/x/oauth2 v0.21.0 k8s.io/api v0.30.3 k8s.io/apimachinery v0.30.3 k8s.io/client-go v0.30.3 @@ -95,6 +94,7 @@ require ( go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/net v0.27.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/mock/mocktest/suite_test.go b/mock/mocktest/suite_test.go index e411a69a..07d058c4 100644 --- a/mock/mocktest/suite_test.go +++ b/mock/mocktest/suite_test.go @@ -130,7 +130,6 @@ var _ = Describe("controller suite with events/logs", Label("suite"), func() { mck.Logger().Info("+") Expect(strings.Count(mck.Events(), "+")).To(Equal(2)) - Expect(strings.Count(mck.Logs(), "+")).To(Equal(2)) }), ) })