github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/runtime/provider.go (about)

     1  package runtime
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/kyma-project/kyma-environment-broker/internal"
    13  	kebError "github.com/kyma-project/kyma-environment-broker/internal/error"
    14  	"github.com/kyma-project/kyma-environment-broker/internal/iosafety"
    15  	"github.com/kyma-project/kyma/components/kyma-operator/pkg/apis/installer/v1alpha1"
    16  	"gopkg.in/yaml.v2"
    17  )
    18  
    19  const (
    20  	releaseInstallerURLFormat  = "https://storage.googleapis.com/kyma-prow-artifacts/%s/kyma-installer-cluster.yaml"
    21  	onDemandInstallerURLFormat = "https://storage.googleapis.com/kyma-development-artifacts/%s/kyma-installer-cluster.yaml"
    22  )
    23  
    24  // ComponentsListProvider provides the whole components list for creating a Kyma Runtime
    25  type ComponentsListProvider struct {
    26  	managedRuntimeComponentsYAMLPath       string
    27  	newAdditionalRuntimeComponentsYAMLPath string
    28  	httpClient                             HTTPDoer
    29  	components                             map[string][]internal.KymaComponent
    30  	mu                                     sync.Mutex
    31  }
    32  
    33  type HTTPDoer interface {
    34  	Do(req *http.Request) (*http.Response, error)
    35  }
    36  
    37  // NewComponentsListProvider returns new instance of the ComponentsListProvider
    38  func NewComponentsListProvider(managedRuntimeComponentsYAMLPath, newAdditionalRuntimeComponentsYAMLPath string) *ComponentsListProvider {
    39  	return &ComponentsListProvider{
    40  		httpClient:                             http.DefaultClient,
    41  		managedRuntimeComponentsYAMLPath:       managedRuntimeComponentsYAMLPath,
    42  		newAdditionalRuntimeComponentsYAMLPath: newAdditionalRuntimeComponentsYAMLPath,
    43  		components:                             make(map[string][]internal.KymaComponent, 0),
    44  	}
    45  }
    46  
    47  // AllComponents returns all components for Kyma Runtime. It fetches always the
    48  // Kyma open-source components from the given url and management components from
    49  // the file system and merge them together.
    50  func (r *ComponentsListProvider) AllComponents(kymaVersion internal.RuntimeVersionData, config *internal.ConfigForPlan) ([]internal.KymaComponent, error) {
    51  	r.mu.Lock()
    52  	defer r.mu.Unlock()
    53  
    54  	if cmps, ok := r.components[kymaVersion.Version]; ok {
    55  		return cmps, nil
    56  	}
    57  
    58  	kymaComponents, err := r.getKymaComponents(kymaVersion)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("while getting Kyma components: %w", err)
    61  	}
    62  
    63  	additionalComponents, err := r.getAdditionalComponents(kymaVersion)
    64  	if err != nil {
    65  		return nil, fmt.Errorf("while getting additional components: %w", err)
    66  	}
    67  
    68  	allComponents := append(kymaComponents, additionalComponents...)
    69  
    70  	r.components[kymaVersion.Version] = allComponents
    71  	return allComponents, nil
    72  }
    73  
    74  func (r *ComponentsListProvider) getKymaComponents(kymaVersion internal.RuntimeVersionData) (comp []internal.KymaComponent, err error) {
    75  	if kymaVersion.MajorVersion > 1 {
    76  		return r.getComponentsFromComponentsYaml(kymaVersion.Version)
    77  	}
    78  	return r.getComponentsFromInstallerYaml(kymaVersion.Version)
    79  }
    80  
    81  func (r *ComponentsListProvider) getAdditionalComponents(kymaVersion internal.RuntimeVersionData) ([]internal.KymaComponent, error) {
    82  	if kymaVersion.MajorVersion > 1 {
    83  		return r.getAdditionalComponentsForNewKyma()
    84  	}
    85  	return r.getAdditionalComponentsForKyma()
    86  }
    87  
    88  // Installation represents the installer CR.
    89  // It is copied because using directly the installer CR
    90  // with such fields:
    91  //
    92  //	metav1.TypeMeta   `json:",inline"`
    93  //	metav1.ObjectMeta `json:"metadata,omitempty"`
    94  //
    95  // is not working with "gopkg.in/yaml.v2" stream decoder.
    96  // On the other hand "sigs.k8s.io/yaml" does not support
    97  // stream decoding.
    98  type Installation struct {
    99  	Kind string                    `json:"kind"`
   100  	Spec v1alpha1.InstallationSpec `json:"spec"`
   101  }
   102  
   103  type kymaComponents struct {
   104  	DefaultNamespace string                   `yaml:"defaultNamespace"`
   105  	Prerequisites    []internal.KymaComponent `yaml:"prerequisites"`
   106  	Components       []internal.KymaComponent `yaml:"components"`
   107  }
   108  
   109  func (r *ComponentsListProvider) checkStatusCode(resp *http.Response) error {
   110  	if resp.StatusCode == http.StatusOK {
   111  		return nil
   112  	}
   113  
   114  	// limited buff to ready only ~4kb, so big response will not blowup our component
   115  	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 4096))
   116  	if err != nil {
   117  		body = []byte(fmt.Sprintf("cannot read body, got error: %s", err))
   118  	}
   119  	msg := fmt.Sprintf("while checking response status code for Kyma components list: "+
   120  		"got unexpected status code, want %d, got %d, url: %s, body: %s",
   121  		http.StatusOK, resp.StatusCode, resp.Request.URL.String(), body)
   122  
   123  	switch {
   124  	case resp.StatusCode == http.StatusRequestTimeout:
   125  		return kebError.NewTemporaryError(msg)
   126  	case resp.StatusCode >= http.StatusInternalServerError:
   127  		return kebError.NewTemporaryError(msg)
   128  	default:
   129  		return fmt.Errorf(msg)
   130  	}
   131  }
   132  
   133  // isOnDemandRelease returns true if the version is recognized as on-demand.
   134  //
   135  // Detection rules:
   136  //
   137  //	For pull requests: PR-<number>
   138  //	For changes to the main branch: main-<commit_sha>
   139  //
   140  // source: https://github.com/kyma-project/test-infra/blob/main/docs/prow/prow-architecture.md#generate-development-artifacts
   141  func (r *ComponentsListProvider) isOnDemandRelease(version string) bool {
   142  	isOnDemandVersion := strings.HasPrefix(version, "PR-") ||
   143  		strings.HasPrefix(version, "main-")
   144  	return isOnDemandVersion
   145  }
   146  
   147  func (r *ComponentsListProvider) getInstallerYamlURL(kymaVersion string) string {
   148  	if r.isOnDemandRelease(kymaVersion) {
   149  		return fmt.Sprintf(onDemandInstallerURLFormat, kymaVersion)
   150  	}
   151  	return fmt.Sprintf(releaseInstallerURLFormat, kymaVersion)
   152  }
   153  
   154  func (r *ComponentsListProvider) getComponentsYamlURL(kymaVersion string) string {
   155  	if r.isOnDemandRelease(kymaVersion) {
   156  		return fmt.Sprintf(onDemandComponentsURLFormat, kymaVersion)
   157  	}
   158  	return fmt.Sprintf(releaseComponentsURLFormat, kymaVersion)
   159  }
   160  
   161  func (r *ComponentsListProvider) getComponentsFromComponentsYaml(kymaVersion string) ([]internal.KymaComponent, error) {
   162  	yamlURL := r.getComponentsYamlURL(kymaVersion)
   163  
   164  	req, err := http.NewRequest(http.MethodGet, yamlURL, nil)
   165  	if err != nil {
   166  		return nil, fmt.Errorf("while creating http request: %w", err)
   167  	}
   168  
   169  	resp, err := r.httpClient.Do(req)
   170  	if err != nil {
   171  		return nil, kebError.AsTemporaryError(err, "while making request for Kyma components list")
   172  	}
   173  	defer func() {
   174  		if drainErr := iosafety.DrainReader(resp.Body); drainErr != nil {
   175  			err = multierror.Append(err, fmt.Errorf("while draining response body: %w", drainErr))
   176  		}
   177  
   178  		if closeErr := resp.Body.Close(); closeErr != nil {
   179  			err = multierror.Append(err, fmt.Errorf("while closing response body: %w", closeErr))
   180  		}
   181  	}()
   182  
   183  	if err = r.checkStatusCode(resp); err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	dec := yaml.NewDecoder(resp.Body)
   188  
   189  	var kymaCmps kymaComponents
   190  	if err = dec.Decode(&kymaCmps); err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	allKymaComponents := make([]internal.KymaComponent, 0)
   195  	allKymaComponents = append(allKymaComponents, kymaCmps.Prerequisites...)
   196  	allKymaComponents = append(allKymaComponents, kymaCmps.Components...)
   197  
   198  	for i, cmp := range allKymaComponents {
   199  		if cmp.Namespace == "" {
   200  			allKymaComponents[i].Namespace = kymaCmps.DefaultNamespace
   201  		}
   202  	}
   203  
   204  	return allKymaComponents, nil
   205  }
   206  
   207  func (r *ComponentsListProvider) getComponentsFromInstallerYaml(kymaVersion string) ([]internal.KymaComponent, error) {
   208  	yamlURL := r.getInstallerYamlURL(kymaVersion)
   209  
   210  	req, err := http.NewRequest(http.MethodGet, yamlURL, nil)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("while creating http request: %w", err)
   213  	}
   214  
   215  	resp, err := r.httpClient.Do(req)
   216  	if err != nil {
   217  		return nil, kebError.AsTemporaryError(err, "while making request for Kyma components list")
   218  	}
   219  	defer func() {
   220  		if drainErr := iosafety.DrainReader(resp.Body); drainErr != nil {
   221  			err = multierror.Append(err, fmt.Errorf("while draining response body: %w", drainErr))
   222  		}
   223  
   224  		if closeErr := resp.Body.Close(); closeErr != nil {
   225  			err = multierror.Append(err, fmt.Errorf("while closing response body: %w", closeErr))
   226  		}
   227  	}()
   228  
   229  	if err := r.checkStatusCode(resp); err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	dec := yaml.NewDecoder(resp.Body)
   234  
   235  	var t Installation
   236  	for dec.Decode(&t) == nil {
   237  		if t.Kind == "Installation" {
   238  			components := make([]internal.KymaComponent, len(t.Spec.Components))
   239  			for i, cmp := range t.Spec.Components {
   240  				components[i] = r.v1alpha1ToKymaComponent(cmp)
   241  			}
   242  			return components, nil
   243  		}
   244  	}
   245  	return nil, fmt.Errorf("installer cr not found")
   246  }
   247  
   248  func (r *ComponentsListProvider) getAdditionalComponentsForNewKyma() ([]internal.KymaComponent, error) {
   249  	yamlContents, err := r.readYAML(r.newAdditionalRuntimeComponentsYAMLPath)
   250  	if err != nil {
   251  		return nil, fmt.Errorf("while reading additional components yaml: %w", err)
   252  	}
   253  	return r.getComponentsFromYAML(yamlContents)
   254  }
   255  
   256  func (r *ComponentsListProvider) getAdditionalComponentsForKyma() ([]internal.KymaComponent, error) {
   257  	yamlContents, err := r.readYAML(r.managedRuntimeComponentsYAMLPath)
   258  	if err != nil {
   259  		return nil, fmt.Errorf("while reading additional components yaml: %w", err)
   260  	}
   261  	return r.getComponentsFromYAML(yamlContents)
   262  }
   263  
   264  func (r *ComponentsListProvider) readYAML(yamlFilePath string) ([]byte, error) {
   265  	yamlContents, err := ioutil.ReadFile(yamlFilePath)
   266  	if err != nil {
   267  		return nil, fmt.Errorf("while reading yaml file: %w", err)
   268  	}
   269  	return yamlContents, nil
   270  }
   271  
   272  func (r *ComponentsListProvider) getComponentsFromYAML(yamlFileContents []byte) ([]internal.KymaComponent, error) {
   273  	var components struct {
   274  		Components []internal.KymaComponent `json:"components"`
   275  	}
   276  	err := yaml.Unmarshal(yamlFileContents, &components)
   277  	if err != nil {
   278  		return nil, fmt.Errorf("while unmarshalling YAML file with additional components: %w", err)
   279  	}
   280  	return components.Components, nil
   281  }
   282  
   283  func (r *ComponentsListProvider) v1alpha1ToKymaComponent(cmp v1alpha1.KymaComponent) internal.KymaComponent {
   284  	var source *internal.ComponentSource
   285  	if cmp.Source != nil {
   286  		source.URL = cmp.Source.URL
   287  	}
   288  
   289  	return internal.KymaComponent{
   290  		Name:        cmp.Name,
   291  		ReleaseName: cmp.ReleaseName,
   292  		Namespace:   cmp.Namespace,
   293  		Source:      source,
   294  	}
   295  }