diff --git a/README.md b/README.md index 4a7776ab57..179f4cab15 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,7 @@ Cloud | Mint | Mint + Remove Admin Cred | Passthrough | Manual | Token AWS | Y | 4.4+ | Y | 4.3+ | 4.6+ (expected) Azure | Y | N | Y | Y | N GCP | Y | 4.7+ | Y | Y | N +IBMCloud | N | N | N | Y | N KubeVirt | N | N | Y | N | N OpenStack | N | N | Y | N | N oVirt | N | N | Y | N | N diff --git a/cmd/ccoctl/main.go b/cmd/ccoctl/main.go index fb354c03bf..0d038ea664 100644 --- a/cmd/ccoctl/main.go +++ b/cmd/ccoctl/main.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning/aws" + "github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning/ibmcloud" ) func main() { @@ -15,6 +16,7 @@ func main() { } rootCmd.AddCommand(aws.NewAWSCmd()) + rootCmd.AddCommand(ibmcloud.NewIBMCloudCmd()) if err := rootCmd.Execute(); err != nil { log.Fatal(err) 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..9922e1c8e1 --- /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 is 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 IBM Cloud credentials request. +// +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/cmd/provisioning/ibmcloud/create_secrets.go b/pkg/cmd/provisioning/ibmcloud/create_secrets.go new file mode 100644 index 0000000000..2cd40016f3 --- /dev/null +++ b/pkg/cmd/provisioning/ibmcloud/create_secrets.go @@ -0,0 +1,182 @@ +package ibmcloud + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/yaml" + + credreqv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1" + "github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning" +) + +const ( + secretManifestsTemplate = `apiVersion: v1 +stringData: + ibmcloud_api_key: %s +kind: Secret +metadata: + name: %s + namespace: %s +type: Opaque` + + manifestsDirName = "manifests" +) + +// APIKeyEnvVar is the environment variable name containing an IBM Cloud API key +const APIKeyEnvVar = "IC_API_KEY" + +var ( + // CreateOpts captures the options that affect creation of the generated + // objects. + CreateOpts = options{ + TargetDir: "", + } +) + +// NewCreateSecretsCmd implements the "create-secrets" command for the credentials provisioning +func NewCreateSecretsCmd() *cobra.Command { + createSecretsCmd := &cobra.Command{ + Use: "create-secrets", + Short: "Create credentials objects", + Long: "Creating objects related to cloud credentials", + RunE: createSecretsCmd, + PersistentPreRun: initEnvForCreateCmd, + } + + createSecretsCmd.PersistentFlags().StringVar(&CreateOpts.CredRequestDir, "credentials-requests-dir", "", "Directory containing files of CredentialsRequests (can be created by running 'oc adm release extract --credentials-requests --cloud=ibmcloud' against an OpenShift release image)") + createSecretsCmd.MarkPersistentFlagRequired("credentials-requests-dir") + createSecretsCmd.PersistentFlags().StringVar(&CreateOpts.TargetDir, "output-dir", "", "Directory to place generated files (defaults to current directory)") + + return createSecretsCmd +} + +func createSecretsCmd(cmd *cobra.Command, args []string) error { + apiKey := os.Getenv(APIKeyEnvVar) + if apiKey == "" { + return fmt.Errorf("%s environment variable not set", APIKeyEnvVar) + } + + err := createSecrets(CreateOpts.CredRequestDir, CreateOpts.TargetDir, apiKey) + if err != nil { + return err + } + return nil +} + +func createSecrets(credReqDir string, targetDir string, apiKey string) error { + credRequests, err := getListOfCredentialsRequests(credReqDir) + if err != nil { + return errors.Wrap(err, "Failed to process files containing CredentialsRequests") + } + + for _, cr := range credRequests { + if err := processCredReq(cr, targetDir, apiKey); err != nil { + return errors.Wrap(err, "Failed to process CredentialsReqeust") + } + } + return nil +} + +func getListOfCredentialsRequests(dir string) ([]*credreqv1.CredentialsRequest, error) { + credRequests := []*credreqv1.CredentialsRequest{} + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, file := range files { + f, err := os.Open(filepath.Join(dir, file.Name())) + if err != nil { + return nil, errors.Wrap(err, "Failed to open file") + } + defer f.Close() + decoder := yaml.NewYAMLOrJSONDecoder(f, 4096) + for { + cr := &credreqv1.CredentialsRequest{} + if err := decoder.Decode(cr); err != nil { + if err == io.EOF { + break + } + return nil, errors.Wrap(err, "Failed to decode to CredentialsRequest") + } + credRequests = append(credRequests, cr) + } + + } + + return credRequests, nil +} + +func processCredReq(cr *credreqv1.CredentialsRequest, targetDir, apiKey string) error { + // Decode IBMCloudProviderSpec + codec, err := credreqv1.NewCodec() + if err != nil { + return errors.Wrap(err, "Failed to create credReq codec") + } + + ibmcloudProviderProviderSpec := credreqv1.IBMCloudProviderSpec{} + if err := codec.DecodeProviderSpec(cr.Spec.ProviderSpec, &ibmcloudProviderProviderSpec); err != nil { + return errors.Wrap(err, "Failed to decode the provider spec") + } + + if ibmcloudProviderProviderSpec.Kind != "IBMCloudProviderSpec" { + return fmt.Errorf("CredentialsRequest %s/%s is not of type IBM Cloud", cr.Namespace, cr.Name) + } + + return writeCredReqSecret(cr, targetDir, apiKey) +} + +func writeCredReqSecret(cr *credreqv1.CredentialsRequest, targetDir, apiKey string) error { + manifestsDir := filepath.Join(targetDir, manifestsDirName) + + fileName := fmt.Sprintf("%s-%s-credentials.yaml", cr.Spec.SecretRef.Namespace, cr.Spec.SecretRef.Name) + filePath := filepath.Join(manifestsDir, fileName) + + fileData := fmt.Sprintf(secretManifestsTemplate, apiKey, cr.Spec.SecretRef.Name, cr.Spec.SecretRef.Namespace) + + if err := ioutil.WriteFile(filePath, []byte(fileData), 0600); err != nil { + return errors.Wrap(err, "Failed to save Secret file") + } + + log.Printf("Saved credentials configuration to: %s", filePath) + + return nil +} + +// initEnvForCreateCmd will ensure the destination directory is ready to +// receive the generated files, and will create the directory if necessary. +func initEnvForCreateCmd(cmd *cobra.Command, args []string) { + if CreateOpts.TargetDir == "" { + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("Failed to get current directory: %s", err) + } + + CreateOpts.TargetDir = pwd + } + + fPath, err := filepath.Abs(CreateOpts.TargetDir) + if err != nil { + log.Fatalf("Failed to resolve full path: %s", err) + } + + // create target dir if necessary + err = provisioning.EnsureDir(fPath) + if err != nil { + log.Fatalf("failed to create target directory at %s", fPath) + } + + // create manifests dir if necessary + manifestsDir := filepath.Join(fPath, manifestsDirName) + err = provisioning.EnsureDir(manifestsDir) + if err != nil { + log.Fatalf("failed to create manifests directory at %s", manifestsDir) + } +} diff --git a/pkg/cmd/provisioning/ibmcloud/create_secrets_test.go b/pkg/cmd/provisioning/ibmcloud/create_secrets_test.go new file mode 100644 index 0000000000..d5de6dad88 --- /dev/null +++ b/pkg/cmd/provisioning/ibmcloud/create_secrets_test.go @@ -0,0 +1,146 @@ +package ibmcloud + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning" +) + +const ( + apiKey = "testapiKey" + testDirPrefix = "createtestdir" +) + +func TestCreateSecretsCmd(t *testing.T) { + tests := []struct { + name string + setup func(*testing.T) string + verify func(t *testing.T, tempDirName string) + cleanup func(*testing.T) + expectError bool + }{ + { + name: "CreateSecretsCmd should populate secret with API key environment variable", + setup: func(t *testing.T) string { + os.Setenv(APIKeyEnvVar, apiKey) + tempDirName, err := ioutil.TempDir(os.TempDir(), testDirPrefix) + require.NoError(t, err, "Failed to create temp directory") + + err = testCredentialsRequest(t, "firstcredreq", "namespace1", "secretName1", tempDirName) + require.NoError(t, err, "Errored while setting up test CredReq files") + + return tempDirName + }, + verify: func(t *testing.T, targetDir string) { + manifestsDir := filepath.Join(targetDir, manifestsDirName) + files, err := ioutil.ReadDir(manifestsDir) + require.NoError(t, err, "Unexpected error listing files in manifestsDir") + + assert.Equal(t, 1, len(files), "Should be exactly 1 Secret generated for 1 CredentialsRequest") + + f, err := os.Open(filepath.Join(manifestsDir, files[0].Name())) + require.NoError(t, err, "Unexpected error opening secret file") + defer f.Close() + decoder := yaml.NewYAMLOrJSONDecoder(f, 4096) + secret := &corev1.Secret{} + if err := decoder.Decode(secret); err != nil && err != io.EOF { + require.NoError(t, err, "Unexpected error decoding secret file") + } + assert.Equal(t, apiKey, secret.StringData["ibmcloud_api_key"]) + }, + cleanup: func(t *testing.T) { + return + }, + expectError: false, + }, + { + name: "CreateSecretsCmd with unset API key environment variable should fail", + setup: func(t *testing.T) string { + os.Setenv(APIKeyEnvVar, "") + tempDirName, err := ioutil.TempDir(os.TempDir(), testDirPrefix) + require.NoError(t, err, "Failed to create temp directory") + + err = testCredentialsRequest(t, "firstcredreq", "namespace1", "secretName1", tempDirName) + require.NoError(t, err, "Errored while setting up test CredReq files") + + return tempDirName + }, + verify: func(t *testing.T, targetDir string) { + return + }, + cleanup: func(t *testing.T) { + return + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + credReqDir := test.setup(t) + defer os.RemoveAll(credReqDir) + + targetDir, err := ioutil.TempDir(os.TempDir(), "ibmcloudcreatetest") + require.NoError(t, err, "Unexpected error creating temp dir for test") + + manifestsDir := filepath.Join(targetDir, manifestsDirName) + err = provisioning.EnsureDir(manifestsDir) + require.NoError(t, err, "Unexpected error creating manifests dir for test") + + args := []string{ + fmt.Sprintf("--credentials-request-dir=%s", credReqDir), + fmt.Sprintf("--output-dir=%s", targetDir), + } + CreateOpts.CredRequestDir = credReqDir + CreateOpts.TargetDir = targetDir + err = createSecretsCmd(&cobra.Command{}, args) + + if test.expectError { + require.Error(t, err, "Expected error returned") + } else { + require.NoError(t, err, "Unexpected error creating secrets") + test.verify(t, targetDir) + } + }) + } +} + +func testCredentialsRequest(t *testing.T, crName, targetSecretNamespace, targetSecretName, targetDir string) error { + credReqTemplate := `--- +apiVersion: cloudcredential.openshift.io/v1 +kind: CredentialsRequest +metadata: + name: %s + namespace: openshift-cloud-credential-operator +spec: + providerSpec: + apiVersion: cloudcredential.openshift.io/v1 + kind: IBMCloudProviderSpec + secretRef: + namespace: %s + name: %s + serviceAccountNames: + - testServiceAccount1` + + credReq := fmt.Sprintf(credReqTemplate, crName, targetSecretNamespace, targetSecretName) + + f, err := ioutil.TempFile(targetDir, "testCredReq") + require.NoError(t, err, "error creating temp file for CredentialsRequest") + defer f.Close() + + _, err = f.Write([]byte(credReq)) + require.NoError(t, err, "error while writing out contents of CredentialsRequest file") + + return nil +} diff --git a/pkg/cmd/provisioning/ibmcloud/debug.test b/pkg/cmd/provisioning/ibmcloud/debug.test new file mode 100755 index 0000000000..dfc597da9c Binary files /dev/null and b/pkg/cmd/provisioning/ibmcloud/debug.test differ diff --git a/pkg/cmd/provisioning/ibmcloud/ibmcloud.go b/pkg/cmd/provisioning/ibmcloud/ibmcloud.go new file mode 100644 index 0000000000..e43fdc6727 --- /dev/null +++ b/pkg/cmd/provisioning/ibmcloud/ibmcloud.go @@ -0,0 +1,23 @@ +package ibmcloud + +import ( + "github.com/spf13/cobra" +) + +type options struct { + TargetDir string + CredRequestDir string +} + +// NewIBMCloudCmd implements the "ibmcloud" subcommand for the credentials provisioning +func NewIBMCloudCmd() *cobra.Command { + createCmd := &cobra.Command{ + Use: "ibmcloud", + Short: "Manage credentials objects for IBM Cloud", + Long: "Creating/deleting cloud credentials objects for IBM Cloud", + } + + createCmd.AddCommand(NewCreateSecretsCmd()) + + return createCmd +}