github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/runtime/components_provider.go (about) 1 package runtime 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "strings" 8 "sync" 9 10 "github.com/kyma-project/kyma-environment-broker/internal" 11 kebError "github.com/kyma-project/kyma-environment-broker/internal/error" 12 "github.com/kyma-project/kyma-environment-broker/internal/iosafety" 13 "gopkg.in/yaml.v2" 14 ) 15 16 const ( 17 releaseComponentsURLFormat = "https://github.com/kyma-project/kyma/releases/download/%s/kyma-components.yaml" 18 onDemandComponentsURLFormat = "https://storage.googleapis.com/kyma-development-artifacts/%s/kyma-components.yaml" 19 ) 20 21 // ComponentsProvider provides the list of required and additional components for creating a Kyma Runtime 22 type ComponentsProvider struct { 23 mu sync.Mutex 24 httpClient HTTPDoer 25 } 26 27 // NewComponentsProvider returns new instance of the ComponentsProvider 28 func NewComponentsProvider() *ComponentsProvider { 29 return &ComponentsProvider{ 30 httpClient: http.DefaultClient, 31 } 32 } 33 34 // AllComponents returns all components for Kyma Runtime. It fetches always the 35 // Kyma open-source components from the given url and management components from 36 // ConfigMaps and merge them together 37 func (p *ComponentsProvider) AllComponents(kymaVersion internal.RuntimeVersionData, config *internal.ConfigForPlan) ([]internal.KymaComponent, error) { 38 p.mu.Lock() 39 defer p.mu.Unlock() 40 41 kymaComponents, err := p.requiredComponents(kymaVersion) 42 if err != nil { 43 return nil, fmt.Errorf("while getting Kyma components: %w", err) 44 } 45 46 additionalComponents := config.AdditionalComponents 47 if err != nil { 48 return nil, fmt.Errorf("while getting additional components: %w", err) 49 } 50 51 allComponents := append(kymaComponents, additionalComponents...) 52 53 return allComponents, nil 54 55 } 56 57 func (p *ComponentsProvider) requiredComponents(kymaVersion internal.RuntimeVersionData) ([]internal.KymaComponent, 58 error) { 59 return p.getComponentsFromComponentsYaml(kymaVersion.Version) 60 61 } 62 63 func (p *ComponentsProvider) getComponentsFromComponentsYaml(version string) ([]internal.KymaComponent, error) { 64 yamlURL := p.getComponentsYamlURL(version) 65 66 req, err := http.NewRequest(http.MethodGet, yamlURL, nil) 67 if err != nil { 68 return nil, fmt.Errorf("while creating HTTP request: %w", err) 69 } 70 71 resp, err := p.httpClient.Do(req) 72 if err != nil { 73 return nil, kebError.AsTemporaryError(err, "while making HTTP request for Kyma components list") 74 } 75 defer func() { 76 if drainErr := iosafety.DrainReader(resp.Body); drainErr != nil { 77 err = fmt.Errorf("while trying to drain body reader: %w", err) 78 } 79 if closeErr := resp.Body.Close(); closeErr != nil { 80 err = fmt.Errorf("while trying to close response body reader: %w", err) 81 } 82 }() 83 84 if err = p.checkStatusCode(resp); err != nil { 85 return nil, err 86 } 87 88 type kymaComponents struct { 89 DefaultNamespace string `yaml:"defaultNamespace"` 90 Prerequisites []internal.KymaComponent `yaml:"prerequisites"` 91 Components []internal.KymaComponent `yaml:"components"` 92 } 93 94 decoder := yaml.NewDecoder(resp.Body) 95 96 var kymaCmps kymaComponents 97 if err = decoder.Decode(&kymaCmps); err != nil { 98 return nil, fmt.Errorf("while decoding response body: %w", err) 99 } 100 101 requiredComponents := make([]internal.KymaComponent, 0) 102 requiredComponents = append(requiredComponents, kymaCmps.Prerequisites...) 103 requiredComponents = append(requiredComponents, kymaCmps.Components...) 104 105 for i, cmp := range requiredComponents { 106 if cmp.Namespace == "" { 107 requiredComponents[i].Namespace = kymaCmps.DefaultNamespace 108 } 109 } 110 111 return requiredComponents, nil 112 } 113 114 func (p *ComponentsProvider) getComponentsYamlURL(kymaVersion string) string { 115 if p.isOnDemandRelease(kymaVersion) { 116 return fmt.Sprintf(onDemandComponentsURLFormat, kymaVersion) 117 } 118 return fmt.Sprintf(releaseComponentsURLFormat, kymaVersion) 119 } 120 121 // isOnDemandRelease returns true if the version is recognized as on-demand. 122 // 123 // Detection rules: 124 // 125 // For pull requests: PR-<number> 126 // For changes to the main branch: main-<commit_sha> 127 // 128 // source: https://github.com/kyma-project/test-infra/blob/main/docs/prow/prow-architecture.md#generate-development-artifacts 129 func (p *ComponentsProvider) isOnDemandRelease(version string) bool { 130 isOnDemandVersion := strings.HasPrefix(version, "PR-") || 131 strings.HasPrefix(version, "main-") 132 return isOnDemandVersion 133 } 134 135 func (p *ComponentsProvider) checkStatusCode(resp *http.Response) error { 136 if resp.StatusCode == http.StatusOK { 137 return nil 138 } 139 140 // limited buff to ready only ~4kb, so big response will not blowup our component 141 body, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) 142 if err != nil { 143 body = []byte(fmt.Sprintf("cannot read body, got error: %s", err)) 144 } 145 msg := fmt.Sprintf("while checking response status code for Kyma components list: "+ 146 "got unexpected status code, want %d, got %d, url: %s, body: %s", 147 http.StatusOK, resp.StatusCode, resp.Request.URL.String(), body) 148 149 switch { 150 case resp.StatusCode == http.StatusRequestTimeout: 151 return kebError.NewTemporaryError(msg) 152 case resp.StatusCode >= http.StatusInternalServerError: 153 return kebError.NewTemporaryError(msg) 154 default: 155 return fmt.Errorf(msg) 156 } 157 }