mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
<!-- Thank you for opening a PR! Here are some things you need to know before submitting: 1. Please read our developer guideline: https://github.com/redhat-developer/odo/wiki/Developer-Guidelines 2. Label this PR accordingly with the '/kind' line 3. Ensure you have written and ran the appropriate tests: https://github.com/redhat-developer/odo/wiki/Writing-and-running-tests 4. Read how we approve and LGTM each PR: https://github.com/redhat-developer/odo/wiki/PR-Review Documentation: If you are pushing a change to documentation, please read: https://github.com/redhat-developer/odo/wiki/Contributing-to-Docs --> **What type of PR is this:** <!-- Add one of the following kinds: /kind bug /kind cleanup /kind tests /kind documentation Feel free to use other [labels](https://github.com/redhat-developer/odo/labels) as needed. However one of the above labels must be present or the PR will not be reviewed. This instruction is for reviewers as well. --> /kind feature **What does this PR do / why we need it:** This PR does the following: - Moves "registry" to preference - Gets rid of unused preference configuration options - Reorders the parameters for preference for the usage in --help **Which issue(s) this PR fixes:** <!-- Specifying the issue will automatically close it when this PR is merged --> https://github.com/redhat-developer/odo/issues/5402 **PR acceptance criteria:** - [X] Unit test - [X] Integration test - [X] Documentation **How to test changes / Special notes to the reviewer:**
519 lines
16 KiB
Go
519 lines
16 KiB
Go
package component
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/devfile/api/v2/pkg/devfile"
|
|
|
|
v1 "k8s.io/api/apps/v1"
|
|
|
|
"github.com/devfile/library/pkg/devfile/parser"
|
|
parsercommon "github.com/devfile/library/pkg/devfile/parser/data/v2/common"
|
|
"github.com/redhat-developer/odo/pkg/devfile/location"
|
|
"github.com/redhat-developer/odo/pkg/envinfo"
|
|
"github.com/redhat-developer/odo/pkg/kclient"
|
|
"github.com/redhat-developer/odo/pkg/localConfigProvider"
|
|
"github.com/redhat-developer/odo/pkg/service"
|
|
|
|
"github.com/pkg/errors"
|
|
applabels "github.com/redhat-developer/odo/pkg/application/labels"
|
|
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
|
|
"github.com/redhat-developer/odo/pkg/preference"
|
|
urlpkg "github.com/redhat-developer/odo/pkg/url"
|
|
"github.com/redhat-developer/odo/pkg/util"
|
|
servicebinding "github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
)
|
|
|
|
const componentRandomNamePartsMaxLen = 12
|
|
const componentNameMaxRetries = 3
|
|
const componentNameMaxLen = -1
|
|
const NotAvailable = "Not available"
|
|
|
|
const apiVersion = "odo.dev/v1alpha1"
|
|
|
|
// GetComponentDir returns source repo name
|
|
// Parameters:
|
|
// path: source path
|
|
// Returns: directory name
|
|
func GetComponentDir(path string) (string, error) {
|
|
retVal := ""
|
|
if path != "" {
|
|
retVal = filepath.Base(path)
|
|
} else {
|
|
currDir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "unable to generate a random name as getting current directory failed")
|
|
}
|
|
retVal = filepath.Base(currDir)
|
|
}
|
|
retVal = strings.TrimSpace(util.GetDNS1123Name(strings.ToLower(retVal)))
|
|
return retVal, nil
|
|
}
|
|
|
|
// GetDefaultComponentName generates a unique component name
|
|
// Parameters: desired default component name(w/o prefix) and slice of existing component names
|
|
// Returns: Unique component name and error if any
|
|
func GetDefaultComponentName(cfg preference.Client, componentPath string, componentType string, existingComponentList ComponentList) (string, error) {
|
|
var prefix string
|
|
var err error
|
|
|
|
// Get component names from component list
|
|
var existingComponentNames []string
|
|
for _, component := range existingComponentList.Items {
|
|
existingComponentNames = append(existingComponentNames, component.Name)
|
|
}
|
|
|
|
// Create a random generated name for the component to use within Kubernetes
|
|
prefix, err = GetComponentDir(componentPath)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "unable to generate random component name")
|
|
}
|
|
prefix = util.TruncateString(prefix, componentRandomNamePartsMaxLen)
|
|
|
|
// Generate unique name for the component using prefix and unique random suffix
|
|
componentName, err := util.GetRandomName(
|
|
fmt.Sprintf("%s-%s", componentType, prefix),
|
|
componentNameMaxLen,
|
|
existingComponentNames,
|
|
componentNameMaxRetries,
|
|
)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "unable to generate random component name")
|
|
}
|
|
|
|
return util.GetDNS1123Name(componentName), nil
|
|
}
|
|
|
|
// ApplyConfig applies the component config onto component deployment
|
|
// Parameters:
|
|
// client: kclient instance
|
|
// componentConfig: Component configuration
|
|
// envSpecificInfo: Component environment specific information, available if uses devfile
|
|
// Returns:
|
|
// err: Errors if any else nil
|
|
func ApplyConfig(client kclient.ClientInterface, envSpecificInfo envinfo.EnvSpecificInfo) (err error) {
|
|
isRouteSupported := false
|
|
isRouteSupported, err = client.IsRouteSupported()
|
|
if err != nil {
|
|
isRouteSupported = false
|
|
}
|
|
|
|
urlClient := urlpkg.NewClient(urlpkg.ClientOptions{
|
|
Client: client,
|
|
IsRouteSupported: isRouteSupported,
|
|
LocalConfigProvider: &envSpecificInfo,
|
|
})
|
|
|
|
return urlpkg.Push(urlpkg.PushParameters{
|
|
LocalConfigProvider: &envSpecificInfo,
|
|
URLClient: urlClient,
|
|
IsRouteSupported: isRouteSupported,
|
|
})
|
|
}
|
|
|
|
// GetComponentNames retrieves the names of the components in the specified application
|
|
func GetComponentNames(client kclient.ClientInterface, applicationName string) ([]string, error) {
|
|
components, err := GetPushedComponents(client, applicationName)
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
names := make([]string, 0, len(components))
|
|
for name := range components {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names, nil
|
|
}
|
|
|
|
// ListDevfileComponents returns the devfile component matching a selector.
|
|
// The selector could be about selecting components part of an application.
|
|
// There are helpers in "applabels" package for this.
|
|
func ListDevfileComponents(client kclient.ClientInterface, selector string) (ComponentList, error) {
|
|
|
|
var deploymentList []v1.Deployment
|
|
var components []Component
|
|
|
|
// retrieve all the deployments that are associated with this application
|
|
deploymentList, err := client.GetDeploymentFromSelector(selector)
|
|
if err != nil {
|
|
return ComponentList{}, errors.Wrapf(err, "unable to list components")
|
|
}
|
|
|
|
// create a list of object metadata based on the component and application name (extracted from Deployment labels)
|
|
for _, elem := range deploymentList {
|
|
component, err := GetComponent(client, elem.Labels[componentlabels.ComponentLabel], elem.Labels[applabels.ApplicationLabel], client.GetCurrentNamespace())
|
|
if err != nil {
|
|
return ComponentList{}, errors.Wrap(err, "Unable to get component")
|
|
}
|
|
|
|
if !reflect.ValueOf(component).IsZero() {
|
|
components = append(components, component)
|
|
}
|
|
|
|
}
|
|
|
|
compoList := newComponentList(components)
|
|
return compoList, nil
|
|
}
|
|
|
|
// List lists all the devfile components in active application
|
|
func List(client kclient.ClientInterface, applicationSelector string) (ComponentList, error) {
|
|
devfileList, err := ListDevfileComponents(client, applicationSelector)
|
|
if err != nil {
|
|
return ComponentList{}, nil
|
|
}
|
|
return newComponentList(devfileList.Items), nil
|
|
}
|
|
|
|
// GetComponentFromDevfile extracts component's metadata from the specified env info if it exists
|
|
func GetComponentFromDevfile(info *envinfo.EnvSpecificInfo) (Component, parser.DevfileObj, error) {
|
|
if info.Exists() {
|
|
devfile, err := parser.Parse(info.GetDevfilePath())
|
|
if err != nil {
|
|
return Component{}, parser.DevfileObj{}, err
|
|
}
|
|
component, err := getComponentFrom(info, GetComponentTypeFromDevfileMetadata(devfile.Data.GetMetadata()))
|
|
if err != nil {
|
|
return Component{}, parser.DevfileObj{}, err
|
|
}
|
|
components, err := devfile.Data.GetComponents(parsercommon.DevfileOptions{})
|
|
if err != nil {
|
|
return Component{}, parser.DevfileObj{}, err
|
|
}
|
|
for _, cmp := range components {
|
|
if cmp.Container != nil {
|
|
for _, env := range cmp.Container.Env {
|
|
component.Spec.Env = append(component.Spec.Env, corev1.EnvVar{Name: env.Name, Value: env.Value})
|
|
}
|
|
}
|
|
}
|
|
|
|
return component, devfile, nil
|
|
}
|
|
return Component{}, parser.DevfileObj{}, nil
|
|
}
|
|
|
|
// GetComponentTypeFromDevfileMetadata returns component type from the devfile metadata;
|
|
// it could either be projectType or language, if neither of them are set, return 'Not available'
|
|
func GetComponentTypeFromDevfileMetadata(metadata devfile.DevfileMetadata) string {
|
|
var componentType string
|
|
if metadata.ProjectType != "" {
|
|
componentType = metadata.ProjectType
|
|
} else if metadata.Language != "" {
|
|
componentType = metadata.Language
|
|
} else {
|
|
componentType = NotAvailable
|
|
}
|
|
return componentType
|
|
}
|
|
|
|
// GetProjectTypeFromDevfileMetadata returns component type from the devfile metadata
|
|
func GetProjectTypeFromDevfileMetadata(metadata devfile.DevfileMetadata) string {
|
|
var projectType string
|
|
if metadata.ProjectType != "" {
|
|
projectType = metadata.ProjectType
|
|
} else {
|
|
projectType = NotAvailable
|
|
}
|
|
return projectType
|
|
}
|
|
|
|
// GetLanguageFromDevfileMetadata returns component type from the devfile metadata
|
|
func GetLanguageFromDevfileMetadata(metadata devfile.DevfileMetadata) string {
|
|
var language string
|
|
if metadata.Language != "" {
|
|
language = metadata.Language
|
|
} else {
|
|
language = NotAvailable
|
|
}
|
|
return language
|
|
}
|
|
|
|
func getComponentFrom(info localConfigProvider.LocalConfigProvider, componentType string) (Component, error) {
|
|
if info.Exists() {
|
|
component := newComponentWithType(info.GetName(), componentType)
|
|
|
|
component.Namespace = info.GetNamespace()
|
|
|
|
component.Spec = ComponentSpec{
|
|
App: info.GetApplication(),
|
|
Type: componentType,
|
|
Ports: []string{fmt.Sprintf("%d", info.GetDebugPort())},
|
|
}
|
|
|
|
urls, err := info.ListURLs()
|
|
if err != nil {
|
|
return Component{}, err
|
|
}
|
|
if len(urls) > 0 {
|
|
for _, url := range urls {
|
|
component.Spec.URL = append(component.Spec.URL, url.Name)
|
|
}
|
|
}
|
|
|
|
return component, nil
|
|
}
|
|
return Component{}, nil
|
|
}
|
|
|
|
func ListDevfileComponentsInPath(client kclient.ClientInterface, paths []string) ([]Component, error) {
|
|
var components []Component
|
|
var err error
|
|
for _, path := range paths {
|
|
err = filepath.Walk(path, func(path string, f os.FileInfo, err error) error {
|
|
// we check for .odo/env/env.yaml folder first and then find devfile.yaml, this could be changed
|
|
// TODO: optimise this
|
|
if f != nil && strings.Contains(f.Name(), ".odo") {
|
|
// lets find if there is a devfile and an env.yaml
|
|
dir := filepath.Dir(path)
|
|
data, err := envinfo.NewEnvSpecificInfo(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if the .odo folder doesn't contain a proper env file
|
|
if data.GetName() == "" || data.GetApplication() == "" || data.GetNamespace() == "" {
|
|
return nil
|
|
}
|
|
|
|
// we just want to confirm if the devfile is correct
|
|
_, err = parser.ParseDevfile(parser.ParserArgs{
|
|
Path: location.DevfileLocation(dir),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
con, _ := filepath.Abs(filepath.Dir(path))
|
|
|
|
comp := NewComponent(data.GetName())
|
|
comp.Status.State = StateTypeUnknown
|
|
comp.Spec.App = data.GetApplication()
|
|
comp.Namespace = data.GetNamespace()
|
|
comp.Status.Context = con
|
|
|
|
// since the config file maybe belong to a component of a different project
|
|
if client != nil {
|
|
client.SetNamespace(data.GetNamespace())
|
|
deployment, err := client.GetOneDeployment(comp.Name, comp.Spec.App)
|
|
if err != nil {
|
|
comp.Status.State = StateTypeNotPushed
|
|
} else if deployment != nil {
|
|
comp.Status.State = StateTypePushed
|
|
}
|
|
}
|
|
|
|
components = append(components, comp)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
}
|
|
return components, err
|
|
}
|
|
|
|
// Exists checks whether a component with the given name exists in the current application or not
|
|
// componentName is the component name to perform check for
|
|
// The first returned parameter is a bool indicating if a component with the given name already exists or not
|
|
// The second returned parameter is the error that might occurs while execution
|
|
func Exists(client kclient.ClientInterface, componentName, applicationName string) (bool, error) {
|
|
deploymentName, err := util.NamespaceOpenShiftObject(componentName, applicationName)
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "unable to create namespaced name")
|
|
}
|
|
deployment, _ := client.GetDeploymentByName(deploymentName)
|
|
if deployment != nil {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func GetComponentState(client kclient.ClientInterface, componentName, applicationName string) State {
|
|
// first check if a deployment exists
|
|
c, err := GetPushedComponent(client, componentName, applicationName)
|
|
if err != nil {
|
|
return StateTypeUnknown
|
|
}
|
|
if c != nil {
|
|
return StateTypePushed
|
|
}
|
|
return StateTypeNotPushed
|
|
}
|
|
|
|
// GetComponent provides component definition
|
|
func GetComponent(client kclient.ClientInterface, componentName string, applicationName string, projectName string) (component Component, err error) {
|
|
return getRemoteComponentMetadata(client, componentName, applicationName, true, true)
|
|
}
|
|
|
|
// getRemoteComponentMetadata provides component metadata from the cluster
|
|
func getRemoteComponentMetadata(client kclient.ClientInterface, componentName string, applicationName string, getUrls, getStorage bool) (Component, error) {
|
|
fromCluster, err := GetPushedComponent(client, componentName, applicationName)
|
|
if err != nil || fromCluster == nil {
|
|
return Component{}, errors.Wrapf(err, "unable to get remote metadata for %s component", componentName)
|
|
}
|
|
|
|
// Component Type
|
|
componentType, err := fromCluster.GetType()
|
|
if err != nil {
|
|
return Component{}, errors.Wrap(err, "unable to get source type")
|
|
}
|
|
|
|
// init component
|
|
component := newComponentWithType(componentName, componentType)
|
|
|
|
// URL
|
|
if getUrls {
|
|
urls, e := fromCluster.GetURLs()
|
|
if e != nil {
|
|
return Component{}, e
|
|
}
|
|
component.Spec.URLSpec = urls
|
|
urlsNb := len(urls)
|
|
if urlsNb > 0 {
|
|
res := make([]string, 0, urlsNb)
|
|
for _, url := range urls {
|
|
res = append(res, url.Name)
|
|
}
|
|
component.Spec.URL = res
|
|
}
|
|
}
|
|
|
|
// Storage
|
|
if getStorage {
|
|
appStore, e := fromCluster.GetStorage()
|
|
if e != nil {
|
|
return Component{}, errors.Wrap(e, "unable to get storage list")
|
|
}
|
|
|
|
component.Spec.StorageSpec = appStore
|
|
var storageList []string
|
|
for _, store := range appStore {
|
|
storageList = append(storageList, store.Name)
|
|
}
|
|
component.Spec.Storage = storageList
|
|
}
|
|
|
|
// Environment Variables
|
|
envVars := fromCluster.GetEnvVars()
|
|
var filteredEnv []corev1.EnvVar
|
|
for _, env := range envVars {
|
|
if !strings.Contains(env.Name, "ODO") {
|
|
filteredEnv = append(filteredEnv, env)
|
|
}
|
|
}
|
|
|
|
// Secrets
|
|
linkedSecrets := fromCluster.GetLinkedSecrets()
|
|
err = setLinksServiceNames(client, linkedSecrets, componentlabels.GetSelector(componentName, applicationName))
|
|
if err != nil {
|
|
return Component{}, fmt.Errorf("unable to get name of services: %w", err)
|
|
}
|
|
component.Status.LinkedServices = linkedSecrets
|
|
|
|
// Annotations
|
|
component.Annotations = fromCluster.GetAnnotations()
|
|
|
|
// Labels
|
|
component.Labels = fromCluster.GetLabels()
|
|
|
|
component.Namespace = client.GetCurrentNamespace()
|
|
component.Spec.App = applicationName
|
|
component.Spec.Env = filteredEnv
|
|
component.Status.State = StateTypePushed
|
|
|
|
return component, nil
|
|
}
|
|
|
|
// setLinksServiceNames sets the service name of the links from the info in ServiceBindingRequests present in the cluster
|
|
func setLinksServiceNames(client kclient.ClientInterface, linkedSecrets []SecretMount, selector string) error {
|
|
ok, err := client.IsServiceBindingSupported()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to check if service binding is supported: %w", err)
|
|
}
|
|
|
|
serviceBindings := map[string]string{}
|
|
if ok {
|
|
// service binding operator is installed on the cluster
|
|
list, err := client.ListDynamicResource(kclient.ServiceBindingGroup, kclient.ServiceBindingVersion, kclient.ServiceBindingResource)
|
|
if err != nil || list == nil {
|
|
return err
|
|
}
|
|
|
|
for _, u := range list.Items {
|
|
var sbr servicebinding.ServiceBinding
|
|
js, err := u.MarshalJSON()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(js, &sbr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
services := sbr.Spec.Services
|
|
if len(services) != 1 {
|
|
return errors.New("the ServiceBinding resource should define only one service")
|
|
}
|
|
service := services[0]
|
|
if service.Kind == "Service" {
|
|
serviceBindings[sbr.Status.Secret] = service.Name
|
|
} else {
|
|
serviceBindings[sbr.Status.Secret] = service.Kind + "/" + service.Name
|
|
}
|
|
}
|
|
} else {
|
|
// service binding operator is not installed
|
|
// get the secrets instead of the service binding objects to retrieve the link data
|
|
secrets, err := client.ListSecrets(selector)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// get the services to get their names against the component names
|
|
services, err := client.ListServices("")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
serviceCompMap := make(map[string]string)
|
|
for _, gotService := range services {
|
|
serviceCompMap[gotService.Labels[componentlabels.ComponentLabel]] = gotService.Name
|
|
}
|
|
|
|
for _, secret := range secrets {
|
|
serviceName, serviceOK := secret.Labels[service.ServiceLabel]
|
|
_, linkOK := secret.Labels[service.LinkLabel]
|
|
serviceKind, serviceKindOK := secret.Labels[service.ServiceKind]
|
|
if serviceKindOK && serviceOK && linkOK {
|
|
if serviceKind == "Service" {
|
|
if _, ok := serviceBindings[secret.Name]; !ok {
|
|
serviceBindings[secret.Name] = serviceCompMap[serviceName]
|
|
}
|
|
} else {
|
|
// service name is stored as kind-name in the labels
|
|
parts := strings.SplitN(serviceName, "-", 2)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
serviceName = fmt.Sprintf("%v/%v", parts[0], parts[1])
|
|
if _, ok := serviceBindings[secret.Name]; !ok {
|
|
serviceBindings[secret.Name] = serviceName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for i, linkedSecret := range linkedSecrets {
|
|
linkedSecrets[i].ServiceName = serviceBindings[linkedSecret.SecretName]
|
|
}
|
|
return nil
|
|
}
|