Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/rhoas/pkged.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/commands/rhoas_cluster_bind.adoc

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

10 changes: 9 additions & 1 deletion locales/cluster/bind/active.en.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[cluster.serviceBinding.namespaceInfo]
one = '"Namespace not provided. Using {{.Namespace}} namespace'
one = 'Namespace not provided. Using {{.Namespace}} namespace'

[cluster.serviceBinding.confirm.message]
one = 'Do you want to continue?'
Expand All @@ -19,4 +19,12 @@ one = 'Binding %v with %v app succeeded'
[cluster.serviceBinding.status.message]
one = 'Binding "%v" with "%v" app'

[cluster.serviceBinding.operatorMissing]
one = '''
Operator is not available on the current cluster.
Please remove --force-operator=true if you wish to create resource without operator
Error:
'''

[cluster.serviceBinding.usingOperator]
one = 'Using ServiceBinding Operator to perform binding'
11 changes: 11 additions & 0 deletions locales/cmd/cluster/bind/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ services with.
Bind command detects Kubernetes deployments and inject managed services credentials to them.
For information about what credentials are injected please refer to individual services
Command will inject credentials as files into `/bindings` folder inside your application.

Bind command will create volume inside your deployment or
ServiceBindingOperator resource if you have it installed on your cluster
'''

[cluster.bind.cmd.example]
Expand All @@ -26,5 +29,13 @@ $ rhoas cluster bind --namespace=ns --app-name=myapp
[cluster.bind.flag.appName]
one = '''Name of the kubernetes deployment to bind'''

[cluster.bind.flag.forceOperator.description]
one = '''Use ServiceBindingOperator only and fail if Operator is not installed'''

[cluster.bind.flag.forceSDK.description]
one = '''Use Service Binding SDK and skip ServiceBindingOperator even if installed on the cluster'''



[cluster.bind.error.emptyResponse]
one = '''Server returned empty response for service'''
106 changes: 80 additions & 26 deletions pkg/cluster/serviceBinding.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@ package cluster

import (
"context"
"crypto/rand"
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/redhat-developer/app-services-cli/internal/localizer"
"github.com/redhat-developer/app-services-cli/pkg/color"
"github.com/redhat-developer/app-services-cli/pkg/logging"
sboContext "github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline/context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"os"
"path/filepath"
"time"

"github.com/redhat-developer/app-services-cli/internal/localizer"
"github.com/redhat-developer/service-binding-operator/api/v1alpha1"
"github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline/builder"
sboContext "github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"os"
"path/filepath"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

Expand All @@ -33,42 +33,53 @@ type KubernetesClients struct {

var deploymentResource = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}

func ExecuteServiceBinding(logger logging.Logger, serviceName string, ns string, appName string, forceCreationWithoutAsk bool) error {
type ServiceBindingOptions struct {
ServiceName string
Namespace string
AppName string
ForceCreationWithoutAsk bool
ForceUseOperator bool
ForceUseSDK bool
BindingName string
BindAsFiles bool
}

func ExecuteServiceBinding(logger logging.Logger, options *ServiceBindingOptions) error {
clients, err := client()
if err != nil {
return err
}

ns := options.Namespace
if ns == "" {
ns, _, err = (*clients.clientConfig).Namespace()
if err != nil {
return err
}
logger.Info(&localizer.Config{
logger.Info(localizer.MustLocalize(&localizer.Config{
MessageID: "cluster.serviceBinding.namespaceInfo",
TemplateData: map[string]interface{}{
"Namespace": color.Info(ns),
},
})
}))
}

// Get proper deployment
if appName == "" {
appName, err = fetchAppNameFromCluster(clients, ns)
if options.AppName == "" {
options.AppName, err = fetchAppNameFromCluster(clients, ns)
if err != nil {
return err
}
} else {
_, err = clients.dynamicClient.Resource(deploymentResource).Namespace(ns).Get(context.TODO(), appName, metav1.GetOptions{})
_, err = clients.dynamicClient.Resource(deploymentResource).Namespace(ns).Get(context.TODO(), options.AppName, metav1.GetOptions{})
if err != nil {
return err
}
}

// Print desired action
logger.Info(fmt.Sprintf(localizer.MustLocalizeFromID("cluster.serviceBinding.status.message"), serviceName, appName))
logger.Info(fmt.Sprintf(localizer.MustLocalizeFromID("cluster.serviceBinding.status.message"), options.ServiceName, options.AppName))

if !forceCreationWithoutAsk {
if !options.ForceCreationWithoutAsk {
var shouldContinue bool
confirm := &survey.Confirm{
Message: localizer.MustLocalizeFromID("cluster.serviceBinding.confirm.message"),
Expand All @@ -84,29 +95,29 @@ func ExecuteServiceBinding(logger logging.Logger, serviceName string, ns string,
}

// Check KafkaConnection
_, err = clients.dynamicClient.Resource(AKCResource).Namespace(ns).Get(context.TODO(), serviceName, metav1.GetOptions{})
_, err = clients.dynamicClient.Resource(AKCResource).Namespace(ns).Get(context.TODO(), options.ServiceName, metav1.GetOptions{})
if err != nil {
return errors.New(localizer.MustLocalizeFromID("cluster.serviceBinding.serviceMissing.message"))
}

// Execute binding
err = performBinding(serviceName, appName, ns, clients)
err = performBinding(options, ns, clients, logger)
if err != nil {
return err
}

logger.Info(fmt.Sprintf(localizer.MustLocalizeFromID("cluster.serviceBinding.bindingSuccess"), serviceName, appName))
logger.Info(fmt.Sprintf(localizer.MustLocalizeFromID("cluster.serviceBinding.bindingSuccess"), options.ServiceName, options.AppName))
return nil
}

func performBinding(serviceName string, appName string, ns string, clients *KubernetesClients) error {
func performBinding(options *ServiceBindingOptions, ns string, clients *KubernetesClients, logger logging.Logger) error {
serviceRef := v1alpha1.Service{
NamespacedRef: v1alpha1.NamespacedRef{
Ref: v1alpha1.Ref{
Group: AKCResource.Group,
Version: AKCResource.Version,
Resource: AKCResource.Resource,
Name: serviceName,
Name: options.ServiceName,
},
},
}
Expand All @@ -116,14 +127,22 @@ func performBinding(serviceName string, appName string, ns string, clients *Kube
Group: deploymentResource.Group,
Version: deploymentResource.Version,
Resource: deploymentResource.Resource,
Name: appName,
Name: options.AppName,
},
}

now := time.Now()
if options.BindingName == "" {
randomValue := make([]byte, 2)
_, err := rand.Read(randomValue)
if err != nil {
return err
}
options.BindingName = fmt.Sprintf("%v-%x", options.ServiceName, randomValue)
}

sb := &v1alpha1.ServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%v-%v", serviceName, now.Unix()),
Name: options.BindingName,
Namespace: ns,
},
Spec: v1alpha1.ServiceBindingSpec{
Expand All @@ -134,6 +153,41 @@ func performBinding(serviceName string, appName string, ns string, clients *Kube
}
sb.SetGroupVersionKind(v1alpha1.GroupVersionKind)

if options.ForceUseSDK {
return useSDKForBinding(clients, sb)
}

// Check of operator is installed
_, err := clients.dynamicClient.Resource(v1alpha1.GroupVersionResource).Namespace(ns).
List(context.TODO(), metav1.ListOptions{Limit: 1})

if err != nil {
if options.ForceUseOperator {
return errors.New(localizer.MustLocalizeFromID("cluster.serviceBinding.operatorMissing") + err.Error())
}
logger.Debug("Service binding Operator not available. Will use SDK option for binding")
return useSDKForBinding(clients, sb)
}

return useOperatorForBinding(logger, sb, clients, ns)

}

func useOperatorForBinding(logger logging.Logger, sb *v1alpha1.ServiceBinding, clients *KubernetesClients, ns string) error {
logger.Info(localizer.MustLocalizeFromID("cluster.serviceBinding.usingOperator"))
sbData, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sb)
if err != nil {
return err
}

unstructuredSB := unstructured.Unstructured{Object: sbData}
_, err = clients.dynamicClient.Resource(v1alpha1.GroupVersionResource).Namespace(ns).
Create(context.TODO(), &unstructuredSB, metav1.CreateOptions{})

return err
}

func useSDKForBinding(clients *KubernetesClients, sb *v1alpha1.ServiceBinding) error {
restMapper, err := apiutil.NewDynamicRESTMapper(clients.restConfig)
if err != nil {
return err
Expand Down
33 changes: 22 additions & 11 deletions pkg/cmd/cluster/bind/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ type Options struct {
ignoreContext bool
appName string
selectedKafka string

forceOperator bool
forceSDK bool
bindingName string
}

func NewBindCommand(f *factory.Factory) *cobra.Command {
Expand Down Expand Up @@ -69,12 +73,13 @@ func NewBindCommand(f *factory.Factory) *cobra.Command {
cmd.Flags().BoolVarP(&opts.forceCreationWithoutAsk, "yes", "y", false, localizer.MustLocalizeFromID("cluster.common.flag.yes.description"))
cmd.Flags().StringVarP(&opts.namespace, "namespace", "n", "", localizer.MustLocalizeFromID("cluster.common.flag.namespace.description"))
cmd.Flags().BoolVarP(&opts.ignoreContext, "ignore-context", "", false, localizer.MustLocalizeFromID("cluster.common.flag.ignoreContext.description"))

cmd.Flags().BoolVarP(&opts.forceOperator, "force-operator", "", false, localizer.MustLocalizeFromID("cluster.bind.flag.forceOperator.description"))
cmd.Flags().BoolVarP(&opts.forceSDK, "force-sdk", "", false, localizer.MustLocalizeFromID("cluster.bind.flag.forceSDK.description"))
return cmd
}

func runBind(opts *Options) error {
connection, err := opts.Connection(connection.DefaultConfigSkipMasAuth)
apiConnection, err := opts.Connection(connection.DefaultConfigSkipMasAuth)
if err != nil {
return err
}
Expand All @@ -92,7 +97,7 @@ func runBind(opts *Options) error {
// In future config will include Id's of other services
if cfg.Services.Kafka == nil || opts.ignoreContext {
// nolint
selectedKafka, err := kafka.InteractiveSelect(connection, logger)
selectedKafka, err := kafka.InteractiveSelect(apiConnection, logger)
if err != nil {

return err
Expand All @@ -102,21 +107,27 @@ func runBind(opts *Options) error {
opts.selectedKafka = cfg.Services.Kafka.ClusterID
}

api := connection.API()
kafkaInstance, _, error := api.Kafka().GetKafkaById(context.Background(), opts.selectedKafka).Execute()
api := apiConnection.API()
kafkaInstance, _, err2 := api.Kafka().GetKafkaById(context.Background(), opts.selectedKafka).Execute()

if error != nil {
return error
if err2 != nil {
return err2
}

if kafkaInstance.Name == nil {
return errors.New(localizer.MustLocalizeFromID("cluster.bind.error.emptyResponse"))
}

err = cluster.ExecuteServiceBinding(logger, *kafkaInstance.Name,
opts.namespace,
opts.appName,
opts.forceCreationWithoutAsk)
err = cluster.ExecuteServiceBinding(logger, &cluster.ServiceBindingOptions{
ServiceName: *kafkaInstance.Name,
Namespace: opts.namespace,
AppName: opts.appName,
ForceCreationWithoutAsk: opts.forceCreationWithoutAsk,
ForceUseOperator: opts.forceOperator,
ForceUseSDK: opts.forceSDK,
BindingName: opts.bindingName,
BindAsFiles: true,
})

return err
}