diff --git a/README.md b/README.md index 4a7776ab57..f01db6a388 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ instance roles) # Cloud Providers -Currently the operator supports AWS, Azure, GCP, KubeVirt, OpenStack. oVirt and VMWare. +Currently the operator supports AWS, Azure, GCP, IBMCloud, KubeVirt, OpenStack. oVirt and VMWare. ## Credentials Root Secret Formats @@ -71,6 +71,18 @@ data: service_account.json: Base64encodeServiceAccount ``` +### IBMCloud + +```yaml +apiVersion: v1 +kind: Secret +metadata: + namespace: kube-system + name: ibmcloud-credentials +data: + ibmcloud_api_key: Base64encodeAPIKey +``` + ### Kubevirt ```yaml @@ -174,7 +186,7 @@ Cons: * Credential permissions may need to be manually updated prior to any upgrade. * Each component has permissions used by all other components. -Supported clouds: AWS, GCP, Azure, VMWare, OpenStack, oVirt, KubeVirt +Supported clouds: AWS, GCP, IBMCloud, Azure, VMWare, OpenStack, oVirt, KubeVirt ## 3. Manual Credentials Management diff --git a/pkg/apis/cloudcredential/v1/register.go b/pkg/apis/cloudcredential/v1/register.go index 43fed04946..addd2d4856 100644 --- a/pkg/apis/cloudcredential/v1/register.go +++ b/pkg/apis/cloudcredential/v1/register.go @@ -57,6 +57,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &AWSProviderStatus{}, &AWSProviderSpec{}, &AzureProviderStatus{}, &AzureProviderSpec{}, &GCPProviderStatus{}, &GCPProviderSpec{}, + &IBMCloudProviderStatus{}, &IBMCloudProviderSpec{}, &VSphereProviderStatus{}, &VSphereProviderSpec{}, &KubevirtProviderStatus{}, &KubevirtProviderSpec{}, ) diff --git a/pkg/apis/cloudcredential/v1/types_ibmcloud.go b/pkg/apis/cloudcredential/v1/types_ibmcloud.go new file mode 100644 index 0000000000..8e5f9a8bbb --- /dev/null +++ b/pkg/apis/cloudcredential/v1/types_ibmcloud.go @@ -0,0 +1,35 @@ +/* +Copyright 2021 The OpenShift Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TODO: these types should eventually be broken out, along with the actuator, to a separate repo. + +// IBMCloudProviderSpec the specification of the credentials request in IBM Cloud. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type IBMCloudProviderSpec struct { + metav1.TypeMeta `json:",inline"` +} + +// IBMCloudProviderStatus contains the status of the credentials request in IBM Cloud. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type IBMCloudProviderStatus struct { + metav1.TypeMeta `json:",inline"` +} diff --git a/pkg/apis/cloudcredential/v1/zz_generated.deepcopy.go b/pkg/apis/cloudcredential/v1/zz_generated.deepcopy.go index a0764c350d..b7da1191da 100644 --- a/pkg/apis/cloudcredential/v1/zz_generated.deepcopy.go +++ b/pkg/apis/cloudcredential/v1/zz_generated.deepcopy.go @@ -313,6 +313,56 @@ func (in *GCPProviderStatus) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IBMCloudProviderSpec) DeepCopyInto(out *IBMCloudProviderSpec) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMCloudProviderSpec. +func (in *IBMCloudProviderSpec) DeepCopy() *IBMCloudProviderSpec { + if in == nil { + return nil + } + out := new(IBMCloudProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IBMCloudProviderSpec) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IBMCloudProviderStatus) DeepCopyInto(out *IBMCloudProviderStatus) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMCloudProviderStatus. +func (in *IBMCloudProviderStatus) DeepCopy() *IBMCloudProviderStatus { + if in == nil { + return nil + } + out := new(IBMCloudProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IBMCloudProviderStatus) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubevirtProviderSpec) DeepCopyInto(out *KubevirtProviderSpec) { *out = *in diff --git a/pkg/ibmcloud/actuator.go b/pkg/ibmcloud/actuator.go new file mode 100644 index 0000000000..aaafc16b53 --- /dev/null +++ b/pkg/ibmcloud/actuator.go @@ -0,0 +1,283 @@ +/* +Copyright 2021 The OpenShift Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package ibmcloud + +import ( + "context" + "fmt" + "reflect" + + log "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1" + "github.com/openshift/cloud-credential-operator/pkg/operator/constants" + actuatoriface "github.com/openshift/cloud-credential-operator/pkg/operator/credentialsrequest/actuator" +) + +// IBMCloudActuator is the IBM Cloud actuator. +type IBMCloudActuator struct { + Client client.Client +} + +const ( + IBMCloudCredentialsSecretKey = "ibmcloud_api_key" +) + +// NewActuator creates a new IBMCloud actuator. +func NewActuator(client client.Client) (*IBMCloudActuator, error) { + return &IBMCloudActuator{ + Client: client, + }, nil +} + +// Exists checks if the credentials currently exist. +// TODO: in the future validate the expiration of the credentials +func (a *IBMCloudActuator) Exists(ctx context.Context, cr *minterv1.CredentialsRequest) (bool, error) { + logger := a.getLogger(cr) + logger.Debug("running Exists") + + existingSecret, err := a.getSecret(ctx, cr, logger) + if err != nil { + return false, err + } + + return existingSecret != nil, nil +} + +// Create the credentials. +func (a *IBMCloudActuator) Create(ctx context.Context, cr *minterv1.CredentialsRequest) error { + logger := a.getLogger(cr) + logger.Debug("running Create") + return a.sync(ctx, cr, logger) +} + +// Update the credentials to the provided definition. +func (a *IBMCloudActuator) Update(ctx context.Context, cr *minterv1.CredentialsRequest) error { + logger := a.getLogger(cr) + logger.Debug("running Update") + return a.sync(ctx, cr, logger) +} + +// Delete credentials +func (a *IBMCloudActuator) Delete(ctx context.Context, cr *minterv1.CredentialsRequest) error { + logger := a.getLogger(cr) + logger.Debug("running Delete") + + existingSecret, err := a.getSecret(ctx, cr, logger) + if err != nil { + return err + } + if existingSecret != nil { + logger.Info("Deleting existing secret") + if err = a.Client.Delete(ctx, existingSecret); err != nil { + return err + } + } + + return nil +} + +// GetCredentialsRootSecretLocation returns the namespace and name where the parent credentials secret is stored. +func (a *IBMCloudActuator) GetCredentialsRootSecretLocation() types.NamespacedName { + return types.NamespacedName{Namespace: constants.CloudCredSecretNamespace, Name: constants.IBMCloudCredSecretName} +} + +// GetCredentialsRootSecret returns the root secret of the credentials request. +func (a *IBMCloudActuator) GetCredentialsRootSecret(ctx context.Context, cr *minterv1.CredentialsRequest) (*corev1.Secret, error) { + logger := a.getLogger(cr) + + // get the secret of the ibmcloud credentials + ibmcloudCredentialsSecret := &corev1.Secret{} + if err := a.Client.Get(ctx, a.GetCredentialsRootSecretLocation(), ibmcloudCredentialsSecret); err != nil { + msg := "unable to fetch root cloud cred secret" + logger.WithError(err).Error(msg) + return nil, &actuatoriface.ActuatorError{ + ErrReason: minterv1.CredentialsProvisionFailure, + Message: fmt.Sprintf("%v: %v", msg, err), + } + } + + return ibmcloudCredentialsSecret, nil +} + +func (a *IBMCloudActuator) sync(ctx context.Context, cr *minterv1.CredentialsRequest, logger log.FieldLogger) error { + logger.Debug("running sync") + + credentialsRootSecret, err := a.GetCredentialsRootSecret(context.TODO(), cr) + if err != nil { + logger.WithError(err).Error("issue with cloud credentials secret") + return err + } + + // get the secret data from the credentials request + ibmcloudCredentialData, err := a.getCredentialsSecretData(credentialsRootSecret, logger) + if err != nil { + logger.WithError(err).Error("issue with cloud credentials secret") + return err + } + + // get the existing secret in order to check if need to update or create a new + logger.Debug("provisioning secret") + existingSecret, err := a.getSecret(ctx, cr, logger) + if err != nil { + return err + } + + // check if need to update or create a new one + if err = a.syncCredentialSecret(ctx, cr, &ibmcloudCredentialData, existingSecret, logger); err != nil { + msg := "error creating/updating secret" + logger.WithError(err).Error(msg) + return &actuatoriface.ActuatorError{ + ErrReason: minterv1.CredentialsProvisionFailure, + Message: fmt.Sprintf("%v: %v", msg, err), + } + } + return nil +} + +func (a *IBMCloudActuator) getCredentialsSecretData(cloudCredSecret *corev1.Secret, logger log.FieldLogger) ([]byte, error) { + // get the secret data - the api key + infraClusterKubeconfig, ok := cloudCredSecret.Data[IBMCloudCredentialsSecretKey] + if !ok { + return nil, fmt.Errorf("IBMCloud credentials secret %s did not contain key %s", cloudCredSecret.Name, IBMCloudCredentialsSecretKey) + } + + logger.Debug("extracted ibmcloud credentials") + return infraClusterKubeconfig, nil +} + +func (a *IBMCloudActuator) syncCredentialSecret(ctx context.Context, cr *minterv1.CredentialsRequest, ibmcloudCredentialData *[]byte, existingSecret *corev1.Secret, logger log.FieldLogger) error { + if existingSecret == nil { + if ibmcloudCredentialData == nil { + msg := "new access key secret needed but no key data provided" + logger.Error(msg) + return &actuatoriface.ActuatorError{ + ErrReason: minterv1.CredentialsProvisionFailure, + Message: msg, + } + } + + return a.createNewSecret(logger, cr, ibmcloudCredentialData, ctx) + } + + return a.updateExistingSecret(logger, existingSecret, cr, ibmcloudCredentialData) +} + +func (a *IBMCloudActuator) updateExistingSecret(logger log.FieldLogger, existingSecret *corev1.Secret, cr *minterv1.CredentialsRequest, ibmcloudCredentialData *[]byte) error { + // Update the existing secret: + logger.Debug("updating secret") + origSecret := existingSecret.DeepCopy() + if existingSecret.Annotations == nil { + existingSecret.Annotations = map[string]string{} + } + existingSecret.Annotations[minterv1.AnnotationCredentialsRequest] = fmt.Sprintf("%s/%s", cr.Namespace, cr.Name) + if ibmcloudCredentialData != nil { + existingSecret.Data = map[string][]byte{ + IBMCloudCredentialsSecretKey: *ibmcloudCredentialData, + } + } + + if !reflect.DeepEqual(existingSecret, origSecret) { + logger.Info("target secret has changed, updating") + if err := a.Client.Update(context.TODO(), existingSecret); err != nil { + msg := "error updating secret" + logger.WithError(err).Error(msg) + return &actuatoriface.ActuatorError{ + ErrReason: minterv1.CredentialsProvisionFailure, + Message: msg, + } + } + } else { + logger.Debug("target secret unchanged") + } + + return nil +} + +func (a *IBMCloudActuator) createNewSecret(logger log.FieldLogger, cr *minterv1.CredentialsRequest, ibmcloudCredentialData *[]byte, ctx context.Context) error { + logger.Info("creating secret") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cr.Spec.SecretRef.Name, + Namespace: cr.Spec.SecretRef.Namespace, + Annotations: map[string]string{ + minterv1.AnnotationCredentialsRequest: fmt.Sprintf("%s/%s", cr.Namespace, cr.Name), + }, + }, + Data: map[string][]byte{ + IBMCloudCredentialsSecretKey: *ibmcloudCredentialData, + }, + } + + if err := a.Client.Create(ctx, secret); err != nil { + msg := "error in creating a secret" + logger.WithError(err).Error(msg) + return &actuatoriface.ActuatorError{ + ErrReason: minterv1.CredentialsProvisionFailure, + Message: msg, + } + } + + logger.Info("secret created successfully") + return nil +} + +func (a *IBMCloudActuator) getSecret(ctx context.Context, cr *minterv1.CredentialsRequest, logger log.FieldLogger) (*corev1.Secret, error) { + logger.Debug("running getSecret") + + existingSecret := &corev1.Secret{} + if err := a.Client.Get(ctx, types.NamespacedName{Namespace: cr.Spec.SecretRef.Namespace, Name: cr.Spec.SecretRef.Name}, existingSecret); err != nil { + if errors.IsNotFound(err) { + logger.Debug("target secret does not exist") + return nil, nil + } + return nil, err + } + + if _, ok := existingSecret.Data[IBMCloudCredentialsSecretKey]; !ok { + logger.Warningf("secret did not have expected key: %s", IBMCloudCredentialsSecretKey) + } + + logger.Debug("target secret exists") + return existingSecret, nil +} + +func (a *IBMCloudActuator) getLogger(cr *minterv1.CredentialsRequest) log.FieldLogger { + return log.WithFields(log.Fields{ + "actuator": "IBMCloud", + "targetSecret": fmt.Sprintf("%s/%s", cr.Spec.SecretRef.Namespace, cr.Spec.SecretRef.Name), + "cr": fmt.Sprintf("%s/%s", cr.Namespace, cr.Name), + }) +} + +func (a *IBMCloudActuator) Upgradeable(mode operatorv1.CloudCredentialsMode) *configv1.ClusterOperatorStatusCondition { + upgradeableCondition := &configv1.ClusterOperatorStatusCondition{ + Status: configv1.ConditionTrue, + Type: configv1.OperatorUpgradeable, + } + return upgradeableCondition +} + +func (a *IBMCloudActuator) GetUpcomingCredSecrets() []types.NamespacedName { + return []types.NamespacedName{} +} diff --git a/pkg/ibmcloud/actuator_test.go b/pkg/ibmcloud/actuator_test.go new file mode 100644 index 0000000000..3a51a04e2e --- /dev/null +++ b/pkg/ibmcloud/actuator_test.go @@ -0,0 +1,373 @@ +/* +Copyright 2021 The OpenShift Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ibmcloud_test + +import ( + "context" + "fmt" + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + + minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1" + "github.com/openshift/cloud-credential-operator/pkg/ibmcloud" + "github.com/openshift/cloud-credential-operator/pkg/operator/constants" + "github.com/openshift/cloud-credential-operator/pkg/util" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + kubernetesclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testNamespace = "ibmcloud-cloud-credential-operator" + testCredRequestName = "openshift-machine-api-ibmcloud" + testInfrastructureName = "test-cluster-abcd" + testRandomSuffix = "random" + testOpenshiftMachineAPIIBMCloudNamespace = "openshift-machine-api" +) + +var ( + ibmcloudCredentialData = []byte("data") + + ibmcloudSpec = &minterv1.IBMCloudProviderSpec{} + + ibmcloudCredentialsSecret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.IBMCloudCredSecretName, + Namespace: constants.CloudCredSecretNamespace, + }, + Data: map[string][]byte{ + ibmcloud.IBMCloudCredentialsSecretKey: ibmcloudCredentialData, + }, + } + + ibmcloudOpenshiftMachineApiSecret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.IBMCloudCredSecretName, + Namespace: testOpenshiftMachineAPIIBMCloudNamespace, + }, + Data: map[string][]byte{ + ibmcloud.IBMCloudCredentialsSecretKey: ibmcloudCredentialData, + }, + } +) + +func TestDecodeToUnknown(t *testing.T) { + codec, err := minterv1.NewCodec() + if err != nil { + t.Fatalf("failed to create codec %#v", err) + } + var raw *runtime.RawExtension + aps := minterv1.IBMCloudProviderSpec{} + raw, err = codec.EncodeProviderSpec(&aps) + if err != nil { + t.Fatalf("failed to encode codec %#v", err) + } + unknown := runtime.Unknown{} + err = codec.DecodeProviderStatus(raw, &unknown) + if err != nil { + t.Fatalf("should be able to decode to Unknown %#v", err) + } + if unknown.Kind != reflect.TypeOf(minterv1.IBMCloudProviderSpec{}).Name() { + t.Fatalf("expected decoded kind to be %s but was %s", reflect.TypeOf(minterv1.IBMCloudProviderSpec{}).Name(), unknown.Kind) + } +} + +func TestCreateCR(t *testing.T) { + util.SetupScheme(scheme.Scheme) + tests := []struct { + name string + existing []runtime.Object + credentialsRequest *minterv1.CredentialsRequest + expectedErr error + errRegexp string + validate func(*testing.T, kubernetesclient.Client) + }{ + { + name: "Create CR happy flow", + existing: defaultExistingObjects(), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client) { + cr := getCredRequest(t, c) + assert.NotNil(t, cr) + + secret := getSecret(t, c) + assert.NotNil(t, secret) + + }, + }, + { + name: "Create CR fail on getCredentialSecret kube-system:ibmcloud-credentials", + existing: []runtime.Object{}, + credentialsRequest: testCredentialsRequest(t), + expectedErr: fmt.Errorf("unable to fetch root cloud cred secret: secrets \"ibmcloud-credentials\" not found"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + allObjects := append(test.existing, test.credentialsRequest) + fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, allObjects...) + + actuator, err := ibmcloud.NewActuator(fakeClient) + if err != nil { + assert.Regexp(t, test.errRegexp, err) + assert.Nil(t, actuator) + return + } + testErr := actuator.Create(context.TODO(), test.credentialsRequest) + if test.expectedErr != nil { + assert.Error(t, testErr) + assert.Equal(t, test.expectedErr.Error(), testErr.Error()) + } else { + test.validate(t, fakeClient) + } + + }) + } +} + +func TestDeleteCR(t *testing.T) { + util.SetupScheme(scheme.Scheme) + + tests := []struct { + name string + existing []runtime.Object + credentialsRequest *minterv1.CredentialsRequest + expectedErr error + errRegexp string + validate func(*testing.T, kubernetesclient.Client) + }{ + { + name: "Delete CR happy flow", + existing: existingObjectsAfterCreate(t), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client) {}, + }, + { + name: "Delete CR happy flow - existingSecret not exist", + existing: defaultExistingObjects(), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client) {}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + allObjects := append(test.existing, test.credentialsRequest) + fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, allObjects...) + + actuator, err := ibmcloud.NewActuator(fakeClient) + if err != nil { + assert.Regexp(t, test.errRegexp, err) + assert.Nil(t, actuator) + return + } + testErr := actuator.Delete(context.TODO(), test.credentialsRequest) + if test.expectedErr != nil { + assert.Error(t, testErr) + assert.Equal(t, test.expectedErr.Error(), testErr.Error()) + } else { + test.validate(t, fakeClient) + } + + }) + } +} + +func TestExistsCR(t *testing.T) { + util.SetupScheme(scheme.Scheme) + + tests := []struct { + name string + existing []runtime.Object + credentialsRequest *minterv1.CredentialsRequest + expectedErr error + errRegexp string + validate func(*testing.T, kubernetesclient.Client, bool) + }{ + { + name: "Exists CR happy flow (true)", + existing: existingObjectsAfterCreate(t), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client, isExists bool) { + secret := getSecret(t, c) + assert.Equal(t, isExists, true) + assert.NotNil(t, secret) + }, + }, + { + name: "Non Exists CR happy flow (false)", + existing: defaultExistingObjects(), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client, isExists bool) { + secret := getSecret(t, c) + assert.Equal(t, isExists, false) + assert.Nil(t, secret) + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + allObjects := append(test.existing, test.credentialsRequest) + fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, allObjects...) + + actuator, err := ibmcloud.NewActuator(fakeClient) + if err != nil { + assert.Regexp(t, test.errRegexp, err) + assert.Nil(t, actuator) + return + } + isExists, testErr := actuator.Exists(context.TODO(), test.credentialsRequest) + if test.expectedErr != nil { + assert.Error(t, testErr) + assert.Equal(t, test.expectedErr.Error(), testErr.Error()) + } else { + test.validate(t, fakeClient, isExists) + } + + }) + } +} + +func TestUpdateCR(t *testing.T) { + util.SetupScheme(scheme.Scheme) + + tests := []struct { + name string + existing []runtime.Object + credentialsRequest *minterv1.CredentialsRequest + expectedErr error + errRegexp string + validate func(*testing.T, kubernetesclient.Client) + }{ + { + name: "Update CR happy flow - non exists", + existing: defaultExistingObjects(), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client) { + cr := getCredRequest(t, c) + assert.NotNil(t, cr) + }, + }, + { + name: "Update CR happy flow - exists", + existing: existingObjectsAfterCreate(t), + credentialsRequest: testCredentialsRequest(t), + validate: func(t *testing.T, c kubernetesclient.Client) { + cr := getCredRequest(t, c) + assert.NotNil(t, cr) + }, + }, + { + name: "Update CR fail on getCredentialSecret kube-system:ibmcloud-credentials", + existing: []runtime.Object{}, + credentialsRequest: testCredentialsRequest(t), + expectedErr: fmt.Errorf("unable to fetch root cloud cred secret: secrets \"ibmcloud-credentials\" not found"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + allObjects := append(test.existing, test.credentialsRequest) + fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, allObjects...) + + actuator, err := ibmcloud.NewActuator(fakeClient) + if err != nil { + assert.Regexp(t, test.errRegexp, err) + assert.Nil(t, actuator) + return + } + testErr := actuator.Update(context.TODO(), test.credentialsRequest) + if test.expectedErr != nil { + assert.Error(t, testErr) + assert.Equal(t, test.expectedErr.Error(), testErr.Error()) + } else { + test.validate(t, fakeClient) + } + + }) + } +} + +func getSecret(t *testing.T, c kubernetesclient.Client) *corev1.Secret { + credRequest := getCredRequest(t, c) + + secret := &corev1.Secret{} + err := c.Get(context.TODO(), types.NamespacedName{ + Namespace: credRequest.Spec.SecretRef.Namespace, + Name: credRequest.Spec.SecretRef.Name, + }, secret) + if err != nil && errors.IsNotFound(err) { + return nil + } + + assert.NoError(t, err) + return secret +} + +func getCredRequest(t *testing.T, c kubernetesclient.Client) *minterv1.CredentialsRequest { + cr := &minterv1.CredentialsRequest{} + assert.NoError(t, c.Get(context.TODO(), types.NamespacedName{Namespace: testNamespace, Name: testCredRequestName}, cr)) + return cr +} + +func defaultExistingObjects() []runtime.Object { + objs := []runtime.Object{ + &ibmcloudCredentialsSecret, + } + return objs +} + +func existingObjectsAfterCreate(t *testing.T) []runtime.Object { + objs := []runtime.Object{ + &ibmcloudCredentialsSecret, + &ibmcloudOpenshiftMachineApiSecret, + } + return objs +} + +func testCredentialsRequest(t *testing.T) *minterv1.CredentialsRequest { + codec, err := minterv1.NewCodec() + if err != nil { + t.Fatalf("error creating ibmcloud codec: %v", err) + } + + rawObj, err := codec.EncodeProviderSpec(ibmcloudSpec) + if err != nil { + t.Fatalf("error decoding provider v1 spec: %v", err) + } + + cr := &minterv1.CredentialsRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testCredRequestName, + }, + Spec: minterv1.CredentialsRequestSpec{ + SecretRef: corev1.ObjectReference{ + Namespace: testOpenshiftMachineAPIIBMCloudNamespace, + Name: constants.IBMCloudCredSecretName, + }, + ProviderSpec: rawObj, + }, + } + + return cr +} diff --git a/pkg/operator/constants/constants.go b/pkg/operator/constants/constants.go index bf63e53d4f..5fce2d3b86 100644 --- a/pkg/operator/constants/constants.go +++ b/pkg/operator/constants/constants.go @@ -113,6 +113,9 @@ const ( // GCPCloudCredSecretName is the name of the secret created by the installer containing cloud creds. GCPCloudCredSecretName = "gcp-credentials" + // IBMCloudCredSecretName is the name of the secret created by the installer containing cloud creds. + IBMCloudCredSecretName = "ibmcloud-credentials" + // OpenStackCloudCredsSecretName is the name of the secret created by the installer containing cloud creds. OpenStackCloudCredsSecretName = "openstack-credentials" diff --git a/pkg/operator/controller.go b/pkg/operator/controller.go index 37bf5d5da9..906cd7f1ef 100644 --- a/pkg/operator/controller.go +++ b/pkg/operator/controller.go @@ -20,6 +20,7 @@ import ( awsactuator "github.com/openshift/cloud-credential-operator/pkg/aws/actuator" "github.com/openshift/cloud-credential-operator/pkg/azure" gcpactuator "github.com/openshift/cloud-credential-operator/pkg/gcp/actuator" + "github.com/openshift/cloud-credential-operator/pkg/ibmcloud" "github.com/openshift/cloud-credential-operator/pkg/kubevirt" "github.com/openshift/cloud-credential-operator/pkg/openstack" "github.com/openshift/cloud-credential-operator/pkg/operator/awspodidentity" @@ -108,6 +109,15 @@ func AddToManager(m manager.Manager, explicitKubeconfig string) error { if err != nil { return err } + case configv1.IBMCloudPlatformType: + log.Info("initializing IBMCloud actuator") + if infraStatus.PlatformStatus == nil || infraStatus.PlatformStatus.IBMCloud == nil { + log.Fatalf("missing IBMCloud configuration in platform status") + } + a, err = ibmcloud.NewActuator(m.GetClient()) + if err != nil { + return err + } case configv1.OvirtPlatformType: log.Info("initializing Ovirt actuator") if infraStatus.PlatformStatus == nil || infraStatus.PlatformStatus.Ovirt == nil { diff --git a/pkg/operator/credentialsrequest/credentialsrequest_controller.go b/pkg/operator/credentialsrequest/credentialsrequest_controller.go index e792252809..5b546b8232 100644 --- a/pkg/operator/credentialsrequest/credentialsrequest_controller.go +++ b/pkg/operator/credentialsrequest/credentialsrequest_controller.go @@ -342,6 +342,7 @@ func isAdminCredSecret(namespace, secretName string) bool { if secretName == constants.AWSCloudCredSecretName || secretName == constants.AzureCloudCredSecretName || secretName == constants.GCPCloudCredSecretName || + secretName == constants.IBMCloudCredSecretName || secretName == constants.OpenStackCloudCredsSecretName || secretName == constants.OvirtCloudCredsSecretName || secretName == constants.KubevirtCloudCredSecretName || @@ -777,6 +778,8 @@ func crInfraMatches(cr *minterv1.CredentialsRequest, clusterCloudPlatform config return cloudType == reflect.TypeOf(minterv1.AzureProviderSpec{}).Name(), nil case configv1.GCPPlatformType: return cloudType == reflect.TypeOf(minterv1.GCPProviderSpec{}).Name(), nil + case configv1.IBMCloudPlatformType: + return cloudType == reflect.TypeOf(minterv1.IBMCloudProviderSpec{}).Name(), nil case configv1.OpenStackPlatformType: return cloudType == reflect.TypeOf(minterv1.OpenStackProviderSpec{}).Name(), nil case configv1.OvirtPlatformType: diff --git a/pkg/operator/metrics/metrics.go b/pkg/operator/metrics/metrics.go index 2abfc1e527..90b9fc9383 100644 --- a/pkg/operator/metrics/metrics.go +++ b/pkg/operator/metrics/metrics.go @@ -169,6 +169,8 @@ func (mc *Calculator) getCloudSecret() (*corev1.Secret, error) { secretKey.Name = constants.AzureCloudCredSecretName case configv1.GCPPlatformType: secretKey.Name = constants.GCPCloudCredSecretName + case configv1.IBMCloudPlatformType: + secretKey.Name = constants.IBMCloudCredSecretName case configv1.OpenStackPlatformType: secretKey.Name = constants.OpenStackCloudCredsSecretName case configv1.OvirtPlatformType: @@ -193,6 +195,8 @@ func cloudProviderSpecToMetricsKey(cloud string) string { return "azure" case "GCPProviderSpec": return "gcp" + case "IBMCloudProviderSpec": + return "ibmcloud" case "OpenStackProviderSpec": return "openstack" case "OvirtProviderSpec": diff --git a/pkg/operator/status/status_controller.go b/pkg/operator/status/status_controller.go index f975ec7adc..ae15340b9a 100644 --- a/pkg/operator/status/status_controller.go +++ b/pkg/operator/status/status_controller.go @@ -394,6 +394,8 @@ func getWatchedSecrets(platformType configv1.PlatformType) []types.NamespacedNam rootSecret.Name = constants.AzureCloudCredSecretName case configv1.GCPPlatformType: rootSecret.Name = constants.GCPCloudCredSecretName + case configv1.IBMCloudPlatformType: + rootSecret.Name = constants.IBMCloudCredSecretName case configv1.OpenStackPlatformType: rootSecret.Name = constants.OpenStackCloudCredsSecretName case configv1.OvirtPlatformType: