cloudsmith.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package cloudsmith
  14. import (
  15. "bytes"
  16. "context"
  17. b64 "encoding/base64"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "io"
  22. "net/http"
  23. "strings"
  24. "time"
  25. "github.com/go-logr/logr"
  26. apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  27. "sigs.k8s.io/controller-runtime/pkg/client"
  28. "sigs.k8s.io/yaml"
  29. genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  30. "github.com/external-secrets/external-secrets/pkg/utils"
  31. )
  32. type Generator struct {
  33. httpClient *http.Client
  34. }
  35. type OIDCRequest struct {
  36. OIDCToken string `json:"oidc_token"`
  37. ServiceSlug string `json:"service_slug"`
  38. }
  39. type OIDCResponse struct {
  40. Token string `json:"token"`
  41. }
  42. const (
  43. defaultCloudsmithAPIURL = "https://api.cloudsmith.io"
  44. errNoSpec = "no config spec provided"
  45. errParseSpec = "unable to parse spec: %w"
  46. errExchangeToken = "unable to exchange OIDC token: %w"
  47. errMarshalRequest = "failed to marshal request payload: %w"
  48. errCreateRequest = "failed to create HTTP request: %w"
  49. errUnexpectedStatus = "request failed due to unexpected status: %s"
  50. errReadResponse = "failed to read response body: %w"
  51. errUnmarshalResponse = "failed to unmarshal response: %w"
  52. errTokenNotFound = "token not found in response"
  53. httpClientTimeout = 30 * time.Second
  54. )
  55. func (g *Generator) Generate(ctx context.Context, cloudsmithSpec *apiextensions.JSON, kubeClient client.Client, targetNamespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
  56. return g.generate(
  57. ctx,
  58. cloudsmithSpec,
  59. kubeClient,
  60. targetNamespace,
  61. )
  62. }
  63. func (g *Generator) Cleanup(_ context.Context, cloudsmithSpec *apiextensions.JSON, providerState genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
  64. return nil
  65. }
  66. func (g *Generator) generate(
  67. ctx context.Context,
  68. cloudsmithSpec *apiextensions.JSON,
  69. _ client.Client,
  70. targetNamespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
  71. if cloudsmithSpec == nil {
  72. return nil, nil, errors.New(errNoSpec)
  73. }
  74. res, err := parseSpec(cloudsmithSpec.Raw)
  75. if err != nil {
  76. return nil, nil, fmt.Errorf(errParseSpec, err)
  77. }
  78. // Fetch the service account token
  79. oidcToken, err := utils.FetchServiceAccountToken(ctx, res.Spec.ServiceAccountRef, targetNamespace)
  80. if err != nil {
  81. return nil, nil, fmt.Errorf("failed to fetch service account token: %w", err)
  82. }
  83. apiURL := res.Spec.APIURL
  84. if apiURL == "" {
  85. apiURL = defaultCloudsmithAPIURL
  86. }
  87. accessToken, err := g.exchangeTokenWithCloudsmith(ctx, oidcToken, res.Spec.OrgSlug, res.Spec.ServiceSlug, apiURL)
  88. if err != nil {
  89. return nil, nil, fmt.Errorf(errExchangeToken, err)
  90. }
  91. exp, err := utils.ExtractJWTExpiration(accessToken)
  92. if err != nil {
  93. return nil, nil, err
  94. }
  95. return map[string][]byte{
  96. "auth": []byte(b64.StdEncoding.EncodeToString([]byte("token:" + accessToken))),
  97. "expiry": []byte(exp),
  98. }, nil, nil
  99. }
  100. func (g *Generator) exchangeTokenWithCloudsmith(ctx context.Context, oidcToken, orgSlug, serviceSlug, apiURL string) (string, error) {
  101. log := logr.FromContextOrDiscard(ctx)
  102. log.V(4).Info("Starting OIDC token exchange with Cloudsmith")
  103. requestPayload := OIDCRequest{
  104. OIDCToken: oidcToken,
  105. ServiceSlug: serviceSlug,
  106. }
  107. jsonPayload, err := json.Marshal(requestPayload)
  108. if err != nil {
  109. return "", fmt.Errorf(errMarshalRequest, err)
  110. }
  111. url := fmt.Sprintf("%s/openid/%s/", strings.TrimSuffix(apiURL, "/"), orgSlug)
  112. log.Info("Exchanging OIDC token with Cloudsmith",
  113. "url", url,
  114. "serviceSlug", serviceSlug,
  115. "orgSlug", orgSlug)
  116. httpClient := g.httpClient
  117. if httpClient == nil {
  118. httpClient = &http.Client{
  119. Timeout: httpClientTimeout,
  120. }
  121. }
  122. req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonPayload))
  123. if err != nil {
  124. return "", fmt.Errorf(errCreateRequest, err)
  125. }
  126. req.Header.Set("Content-Type", "application/json")
  127. resp, err := httpClient.Do(req)
  128. if err != nil {
  129. return "", fmt.Errorf("failed to execute HTTP request: %w", err)
  130. }
  131. defer func() {
  132. _ = resp.Body.Close()
  133. }()
  134. if resp.StatusCode != http.StatusCreated {
  135. return "", fmt.Errorf(errUnexpectedStatus, resp.Status)
  136. }
  137. body, err := io.ReadAll(resp.Body)
  138. if err != nil {
  139. return "", fmt.Errorf(errReadResponse, err)
  140. }
  141. var result OIDCResponse
  142. err = json.Unmarshal(body, &result)
  143. if err != nil {
  144. return "", fmt.Errorf(errUnmarshalResponse, err)
  145. }
  146. if result.Token == "" {
  147. return "", errors.New(errTokenNotFound)
  148. }
  149. log.V(4).Info("Successfully exchanged OIDC token for Cloudsmith access token")
  150. return result.Token, nil
  151. }
  152. func parseSpec(specData []byte) (*genv1alpha1.CloudsmithAccessToken, error) {
  153. var spec genv1alpha1.CloudsmithAccessToken
  154. err := yaml.Unmarshal(specData, &spec)
  155. return &spec, err
  156. }
  157. func init() {
  158. genv1alpha1.Register(genv1alpha1.CloudsmithAccessTokenKind, &Generator{})
  159. }