Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions cmd/ccoctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -15,6 +16,7 @@ func main() {
}

rootCmd.AddCommand(aws.NewAWSCmd())
rootCmd.AddCommand(ibmcloud.NewIBMCloudCmd())

if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/cloudcredential/v1/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&AWSProviderStatus{}, &AWSProviderSpec{},
&AzureProviderStatus{}, &AzureProviderSpec{},
&GCPProviderStatus{}, &GCPProviderSpec{},
&IBMCloudProviderSpec{},
Comment thread
BobbyRadford marked this conversation as resolved.
Outdated
&VSphereProviderStatus{}, &VSphereProviderSpec{},
&KubevirtProviderStatus{}, &KubevirtProviderSpec{},
)
Expand Down
29 changes: 29 additions & 0 deletions pkg/apis/cloudcredential/v1/types_ibmcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
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.
Comment thread
BobbyRadford marked this conversation as resolved.
Outdated
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type IBMCloudProviderSpec struct {
metav1.TypeMeta `json:",inline"`
}
Comment thread
BobbyRadford marked this conversation as resolved.
25 changes: 25 additions & 0 deletions pkg/apis/cloudcredential/v1/zz_generated.deepcopy.go

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

177 changes: 177 additions & 0 deletions pkg/cmd/provisioning/ibmcloud/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package ibmcloud

import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"

credreqv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
"github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep consistency, these imports should come in a separate block at the end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Hopefully I understood you correctly.

"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/yaml"
)

const (
secretManifestsTemplate = `apiVersion: v1
stringData:
ibmcloud_api_key: %s
kind: Secret
metadata:
name: %s
namespace: %s
type: Opaque`

manifestsDirName = "manifests"
)

var (
// CreateOpts captures the options that affect creation of the generated
// objects.
CreateOpts = options{
TargetDir: "",
}
)

// NewCreateCmd implements the "create" command for the credentials provisioning
func NewCreateCmd() *cobra.Command {
createCmd := &cobra.Command{
Use: "create",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by command conventions that we follow, this should be create-secrets I feel. @dgoodwin WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's no problem. Changed to create-secrets

Short: "Create credentials objects",
Long: "Creating objects related to cloud credentials",
Run: createCmd,
PersistentPreRun: initEnvForCreateCmd,
}

createCmd.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)")
createCmd.MarkPersistentFlagRequired("credentials-requests-dir")
createCmd.PersistentFlags().StringVar(&CreateOpts.TargetDir, "output-dir", "", "Directory to place generated files (defaults to current directory)")

return createCmd
}

func createCmd(cmd *cobra.Command, args []string) {
apiKey := os.Getenv("IC_API_KEY")
if apiKey == "" {
log.Fatal(fmt.Errorf("IC_API_KEY environment variable not set"))
}

err := create(CreateOpts.CredRequestDir, CreateOpts.TargetDir, apiKey)
if err != nil {
log.Fatal(err)
}
}

func create(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)
}
}
103 changes: 103 additions & 0 deletions pkg/cmd/provisioning/ibmcloud/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package ibmcloud

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
apiKey = "testapiKey"
testDirPrefix = "createtestdir"
)

func TestIAMRoles(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: "Generate Secret for one CredentialsRequest",
setup: func(t *testing.T) string {
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) {
files, err := ioutil.ReadDir(targetDir)
require.NoError(t, err, "Unexpected error listing files in targetDir")

assert.Equal(t, 1, len(files), "Should be exactly 1 Secret generated for 1 CredentialsRequest")
},
cleanup: func(t *testing.T) {
return
},
expectError: false,
Comment on lines +28 to +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a test that does setting IC_API_KEY in setup and verify if generated secret actually has that key? Also, one negative test where env var is not set and error is thrown.

},
}

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")

err = create(credReqDir, targetDir, apiKey)

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
}
24 changes: 24 additions & 0 deletions pkg/cmd/provisioning/ibmcloud/ibmcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ibmcloud

import (
"github.com/spf13/cobra"
)

type options struct {
TargetDir string
CredRequestDir string
DryRun bool
Comment thread
BobbyRadford marked this conversation as resolved.
Outdated
}

// 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(NewCreateCmd())

return createCmd
}