Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creates service on cluster upon "odo push" #4650

Merged
merged 2 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ require (
k8s.io/cli-runtime v0.17.0
k8s.io/client-go v12.0.0+incompatible
k8s.io/klog v1.0.0
k8s.io/klog/v2 v2.0.0
k8s.io/kubectl v0.0.0
sigs.k8s.io/yaml v1.2.0
)
Expand Down
48 changes: 35 additions & 13 deletions pkg/devfile/adapters/kubernetes/component/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"time"

"github.com/openshift/odo/pkg/service"

"github.com/devfile/library/pkg/devfile/generator"
componentlabels "github.com/openshift/odo/pkg/component/labels"
"github.com/openshift/odo/pkg/envinfo"
Expand Down Expand Up @@ -172,6 +174,26 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
return errors.Wrap(err, "unable to create or update component")
}

// fetch the "kubernetes inlined components" to create them on cluster
// from odo standpoint, these components contain yaml manifest of an odo service or an odo link
k8sComponents, err := a.Devfile.Data.GetComponents(parsercommon.DevfileOptions{
ComponentOptions: parsercommon.ComponentOptions{ComponentType: devfilev1.KubernetesComponentType},
})
if err != nil {
return errors.Wrap(err, "error while trying to fetch service(s) from devfile")
}
// create the Kubernetes objects from the manifest
services, err := service.CreateServiceFromKubernetesInlineComponents(a.Client.GetKubeClient(), k8sComponents)
if err != nil {
return errors.Wrap(err, "failed to create service(s) associated with the component")
}

if len(services) == 1 {
log.Infof("Created service %q on the cluster; refer %q to know how to link it to the component", services[0], "odo link -h")
} else if len(services) > 1 {
log.Infof("Created services %q on the cluster; refer %q to know how to link them to the component", strings.Join(services, ", "), "odo link -h")
}

deployment, err := a.Client.GetKubeClient().WaitForDeploymentRollout(a.ComponentName)
if err != nil {
return errors.Wrap(err, "error while waiting for deployment rollout")
Expand Down Expand Up @@ -376,7 +398,7 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
}

if len(containers) == 0 {
return fmt.Errorf("No valid components found in the devfile")
return fmt.Errorf("no valid components found in the devfile")
}

// Add the project volume before generating init containers
Expand Down Expand Up @@ -471,7 +493,7 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
ObjectMeta: objectMeta,
SelectorLabels: selectorLabels,
}
service, err := generator.GetService(a.Devfile, serviceParams, parsercommon.DevfileOptions{})
svc, err := generator.GetService(a.Devfile, serviceParams, parsercommon.DevfileOptions{})
if err != nil {
return err
}
Expand All @@ -488,21 +510,21 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
klog.V(2).Infof("Successfully updated component %v", componentName)
oldSvc, err := a.Client.GetKubeClient().KubeClient.CoreV1().Services(a.Client.Namespace).Get(componentName, metav1.GetOptions{})
ownerReference := generator.GetOwnerReference(deployment)
service.OwnerReferences = append(service.OwnerReferences, ownerReference)
svc.OwnerReferences = append(svc.OwnerReferences, ownerReference)
if err != nil {
// no old service was found, create a new one
if len(service.Spec.Ports) > 0 {
_, err = a.Client.GetKubeClient().CreateService(*service)
if len(svc.Spec.Ports) > 0 {
_, err = a.Client.GetKubeClient().CreateService(*svc)
if err != nil {
return err
}
klog.V(2).Infof("Successfully created Service for component %s", componentName)
}
} else {
if len(service.Spec.Ports) > 0 {
service.Spec.ClusterIP = oldSvc.Spec.ClusterIP
service.ResourceVersion = oldSvc.GetResourceVersion()
_, err = a.Client.GetKubeClient().UpdateService(*service)
if len(svc.Spec.Ports) > 0 {
svc.Spec.ClusterIP = oldSvc.Spec.ClusterIP
svc.ResourceVersion = oldSvc.GetResourceVersion()
_, err = a.Client.GetKubeClient().UpdateService(*svc)
if err != nil {
return err
}
Expand All @@ -521,9 +543,9 @@ func (a Adapter) createOrUpdateComponent(componentExists bool, ei envinfo.EnvSpe
}
klog.V(2).Infof("Successfully created component %v", componentName)
ownerReference := generator.GetOwnerReference(deployment)
service.OwnerReferences = append(service.OwnerReferences, ownerReference)
if len(service.Spec.Ports) > 0 {
_, err = a.Client.GetKubeClient().CreateService(*service)
svc.OwnerReferences = append(svc.OwnerReferences, ownerReference)
if len(svc.Spec.Ports) > 0 {
_, err = a.Client.GetKubeClient().CreateService(*svc)
if err != nil {
return err
}
Expand All @@ -549,7 +571,7 @@ func getFirstContainerWithSourceVolume(containers []corev1.Container) (string, s
}
}

return "", "", fmt.Errorf("In order to sync files, odo requires at least one component in a devfile to set 'mountSources: true'")
return "", "", fmt.Errorf("in order to sync files, odo requires at least one component in a devfile to set 'mountSources: true'")
}

// Delete deletes the component
Expand Down
6 changes: 3 additions & 3 deletions pkg/odo/cli/component/devfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ func (po *PushOptions) devfilePushInner() (err error) {

// Parse devfile and validate
devObj, err := devfile.ParseDevfileAndValidate(parser.ParserArgs{Path: po.DevfilePath})

if err != nil {
return err
}

// odo specific validations
err = validate.ValidateDevfileData(devObj.Data)
if err != nil {
return err
Expand Down Expand Up @@ -138,12 +138,12 @@ func (po *PushOptions) devfilePushInner() (err error) {
// Start or update the component
err = devfileHandler.Push(pushParams)
if err != nil {
err = errors.Errorf("Failed to start component with name %s. Error: %v",
err = errors.Errorf("Failed to start component with name %q. Error: %v",
componentName,
err,
)
} else {
log.Infof("\nPushing devfile component %s", componentName)
log.Infof("\nPushing devfile component %q", componentName)
log.Success("Changes successfully pushed to component")
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/odo/cli/component/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"fmt"
"path/filepath"

"github.com/openshift/odo/pkg/component"
"github.com/openshift/odo/pkg/log"

"github.com/devfile/library/pkg/devfile"
"github.com/openshift/odo/pkg/devfile/validate"
"github.com/openshift/odo/pkg/envinfo"
ktemplates "k8s.io/kubectl/pkg/util/templates"

"github.com/devfile/library/pkg/devfile/parser"
"github.com/openshift/odo/pkg/component"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/occlient"
projectCmd "github.com/openshift/odo/pkg/odo/cli/project"
"github.com/openshift/odo/pkg/odo/genericclioptions"
Expand Down
30 changes: 15 additions & 15 deletions pkg/odo/cli/service/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,20 +126,6 @@ func (o *CreateOptions) Complete(name string, cmd *cobra.Command, args []string)
return o.Backend.CompleteServiceCreate(o, cmd, args)
}

// outputNonInteractiveEquivalent outputs the populated options as the equivalent command that would be used in non-interactive mode
func (o *CreateOptions) outputNonInteractiveEquivalent() string {
if o.outputCLI {
var tpl bytes.Buffer
t := template.Must(template.New("service-create-cli").Parse(equivalentTemplate))
e := t.Execute(&tpl, o)
if e != nil {
panic(e) // shouldn't happen
}
return strings.TrimSpace(tpl.String())
}
return ""
}

// Validate validates the CreateOptions based on completed values
func (o *CreateOptions) Validate() (err error) {
// if we are in interactive mode, all values are already valid
Expand All @@ -159,7 +145,7 @@ func (o *CreateOptions) Run() (err error) {

// Information on what to do next; don't do this if "--dry-run" was requested as it gets appended to the file
if !o.DryRun {
log.Infof("You can now link the service to a component using 'odo link'; check 'odo link -h'")
log.Info("Successfully added service to the configuration; do 'odo push' to create service on the cluster")
}

equivalent := o.outputNonInteractiveEquivalent()
Expand All @@ -169,6 +155,20 @@ func (o *CreateOptions) Run() (err error) {
return
}

// outputNonInteractiveEquivalent outputs the populated options as the equivalent command that would be used in non-interactive mode
func (o *CreateOptions) outputNonInteractiveEquivalent() string {
if o.outputCLI {
var tpl bytes.Buffer
t := template.Must(template.New("service-create-cli").Parse(equivalentTemplate))
e := t.Execute(&tpl, o)
if e != nil {
panic(e) // shouldn't happen
}
return strings.TrimSpace(tpl.String())
}
return ""
}

// NewCmdServiceCreate implements the odo service create command.
func NewCmdServiceCreate(name, fullName string) *cobra.Command {
o := NewCreateOptions()
Expand Down
92 changes: 7 additions & 85 deletions pkg/odo/cli/service/operator_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,63 +19,6 @@ import (
"github.com/spf13/cobra"
)

// DynamicCRD holds the original CR obtained from the Operator (a CSV), or user
// (when they use --from-file flag), and few other attributes that are likely
// to be used to validate a CRD before creating a service from it
type DynamicCRD struct {
// contains the CR as obtained from CSV or user
OriginalCRD map[string]interface{}
}

func NewDynamicCRD() *DynamicCRD {
return &DynamicCRD{}
}

// validateMetadataInCRD validates if the CRD has metadata.name field and returns an error
func (d *DynamicCRD) validateMetadataInCRD() error {
metadata, ok := d.OriginalCRD["metadata"].(map[string]interface{})
if !ok {
// this condition is satisfied if there's no metadata at all in the provided CRD
return fmt.Errorf("couldn't find \"metadata\" in the yaml; need metadata start the service")
}

if _, ok := metadata["name"].(string); ok {
// found the metadata.name; no error
return nil
}
return fmt.Errorf("couldn't find metadata.name in the yaml; provide a name for the service")
}

// setServiceName modifies the CRD to contain user provided name on the CLI
// instead of using the default one in almExample
func (d *DynamicCRD) setServiceName(name string) {
metaMap := d.OriginalCRD["metadata"].(map[string]interface{})

for k := range metaMap {
if k == "name" {
metaMap[k] = name
return
}
// if metadata doesn't have 'name' field, we set it up
metaMap["name"] = name
}
}

// getServiceNameFromCRD fetches the service name from metadata.name field of the CRD
func (d *DynamicCRD) getServiceNameFromCRD() (string, error) {
metadata, ok := d.OriginalCRD["metadata"].(map[string]interface{})
if !ok {
// this condition is satisfied if there's no metadata at all in the provided CRD
return "", fmt.Errorf("couldn't find \"metadata\" in the yaml; need metadata.name to start the service")
}

if name, ok := metadata["name"].(string); ok {
// found the metadata.name; no error
return name, nil
}
return "", fmt.Errorf("couldn't find metadata.name in the yaml; provide a name for the service")
}

// This CompleteServiceCreate contains logic to complete the "odo service create" call for the case of Operator backend
func (b *OperatorBackend) CompleteServiceCreate(o *CreateOptions, cmd *cobra.Command, args []string) (err error) {
// since interactive mode is not supported for Operators yet, set it to false
Expand Down Expand Up @@ -109,7 +52,7 @@ func (b *OperatorBackend) CompleteServiceCreate(o *CreateOptions, cmd *cobra.Com
}

func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
d := NewDynamicCRD()
d := svc.NewDynamicCRD()
// if the user wants to create service from a file, we check for
// existence of file and validate if the requested operator and CR
// exist on the cluster
Expand Down Expand Up @@ -142,7 +85,7 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
return err
}

err = d.validateMetadataInCRD()
err = d.ValidateMetadataInCRD()
if err != nil {
return err
}
Expand All @@ -158,9 +101,9 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
return fmt.Errorf("service %q already exists; please provide a different name or delete the existing service first", svcFullName)
}

d.setServiceName(o.ServiceName)
d.SetServiceName(o.ServiceName)
} else {
o.ServiceName, err = d.getServiceNameFromCRD()
o.ServiceName, err = d.GetServiceNameFromCRD()
if err != nil {
return err
}
Expand Down Expand Up @@ -202,10 +145,10 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
return fmt.Errorf("service %q already exists; please provide a different name or delete the existing service first", svcFullName)
}

d.setServiceName(o.ServiceName)
d.SetServiceName(o.ServiceName)
}

err = d.validateMetadataInCRD()
err = d.ValidateMetadataInCRD()
if err != nil {
return err
}
Expand All @@ -214,7 +157,7 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
b.CustomResourceDefinition = d.OriginalCRD

if o.ServiceName == "" {
o.ServiceName, err = d.getServiceNameFromCRD()
o.ServiceName, err = d.GetServiceNameFromCRD()
if err != nil {
return err
}
Expand All @@ -235,18 +178,6 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
func (b *OperatorBackend) RunServiceCreate(o *CreateOptions) (err error) {
s := &log.Status{}

// in case of an Operator backed service, name of the service is
// provided by the yaml specification in alm-examples. It might also
// happen that a user wants to spin up Service Catalog based service in
// spite of having 4.x cluster mode but we're not supporting
// interacting with both Operator Hub and Service Catalog on 4.x. So
// the user won't get to see service name in the log message
if !o.DryRun {
log.Infof("Deploying service %q of type: %q", o.ServiceName, b.CustomResource)
s = log.Spinner("Deploying service")
defer s.End(false)
}

// if cluster has resources of type CSV and o.CustomResource is not
// empty, we're expected to create an Operator backed service
if o.DryRun {
Expand All @@ -266,15 +197,6 @@ func (b *OperatorBackend) RunServiceCreate(o *CreateOptions) (err error) {

return nil
} else {
err = svc.CreateOperatorService(o.KClient, b.group, b.version, b.resource, b.CustomResourceDefinition)
if err != nil {
// TODO: logic to remove CRD info from devfile because service creation failed.
return err
} else {
s.End(true)
log.Successf(`Service %q was created`, o.ServiceName)
}

crdYaml, err := yaml.Marshal(b.CustomResourceDefinition)
if err != nil {
return err
Expand Down
Loading