| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- /*
- Copyright © 2025 ESO Maintainer Team
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package cloudsmith
- import (
- "bytes"
- "context"
- b64 "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strings"
- "time"
- "github.com/go-logr/logr"
- apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/yaml"
- genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
- "github.com/external-secrets/external-secrets/pkg/utils"
- )
- type Generator struct {
- httpClient *http.Client
- }
- type OIDCRequest struct {
- OIDCToken string `json:"oidc_token"`
- ServiceSlug string `json:"service_slug"`
- }
- type OIDCResponse struct {
- Token string `json:"token"`
- }
- const (
- defaultCloudsmithAPIURL = "https://api.cloudsmith.io"
- errNoSpec = "no config spec provided"
- errParseSpec = "unable to parse spec: %w"
- errExchangeToken = "unable to exchange OIDC token: %w"
- errMarshalRequest = "failed to marshal request payload: %w"
- errCreateRequest = "failed to create HTTP request: %w"
- errUnexpectedStatus = "request failed due to unexpected status: %s"
- errReadResponse = "failed to read response body: %w"
- errUnmarshalResponse = "failed to unmarshal response: %w"
- errTokenNotFound = "token not found in response"
- httpClientTimeout = 30 * time.Second
- )
- func (g *Generator) Generate(ctx context.Context, cloudsmithSpec *apiextensions.JSON, kubeClient client.Client, targetNamespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
- return g.generate(
- ctx,
- cloudsmithSpec,
- kubeClient,
- targetNamespace,
- )
- }
- func (g *Generator) Cleanup(_ context.Context, cloudsmithSpec *apiextensions.JSON, providerState genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
- return nil
- }
- func (g *Generator) generate(
- ctx context.Context,
- cloudsmithSpec *apiextensions.JSON,
- _ client.Client,
- targetNamespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
- if cloudsmithSpec == nil {
- return nil, nil, errors.New(errNoSpec)
- }
- res, err := parseSpec(cloudsmithSpec.Raw)
- if err != nil {
- return nil, nil, fmt.Errorf(errParseSpec, err)
- }
- // Fetch the service account token
- oidcToken, err := utils.FetchServiceAccountToken(ctx, res.Spec.ServiceAccountRef, targetNamespace)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to fetch service account token: %w", err)
- }
- apiURL := res.Spec.APIURL
- if apiURL == "" {
- apiURL = defaultCloudsmithAPIURL
- }
- accessToken, err := g.exchangeTokenWithCloudsmith(ctx, oidcToken, res.Spec.OrgSlug, res.Spec.ServiceSlug, apiURL)
- if err != nil {
- return nil, nil, fmt.Errorf(errExchangeToken, err)
- }
- exp, err := utils.ExtractJWTExpiration(accessToken)
- if err != nil {
- return nil, nil, err
- }
- return map[string][]byte{
- "auth": []byte(b64.StdEncoding.EncodeToString([]byte("token:" + accessToken))),
- "expiry": []byte(exp),
- }, nil, nil
- }
- func (g *Generator) exchangeTokenWithCloudsmith(ctx context.Context, oidcToken, orgSlug, serviceSlug, apiURL string) (string, error) {
- log := logr.FromContextOrDiscard(ctx)
- log.V(4).Info("Starting OIDC token exchange with Cloudsmith")
- requestPayload := OIDCRequest{
- OIDCToken: oidcToken,
- ServiceSlug: serviceSlug,
- }
- jsonPayload, err := json.Marshal(requestPayload)
- if err != nil {
- return "", fmt.Errorf(errMarshalRequest, err)
- }
- url := fmt.Sprintf("%s/openid/%s/", strings.TrimSuffix(apiURL, "/"), orgSlug)
- log.Info("Exchanging OIDC token with Cloudsmith",
- "url", url,
- "serviceSlug", serviceSlug,
- "orgSlug", orgSlug)
- httpClient := g.httpClient
- if httpClient == nil {
- httpClient = &http.Client{
- Timeout: httpClientTimeout,
- }
- }
- req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonPayload))
- if err != nil {
- return "", fmt.Errorf(errCreateRequest, err)
- }
- req.Header.Set("Content-Type", "application/json")
- resp, err := httpClient.Do(req)
- if err != nil {
- return "", fmt.Errorf("failed to execute HTTP request: %w", err)
- }
- defer func() {
- _ = resp.Body.Close()
- }()
- if resp.StatusCode != http.StatusCreated {
- return "", fmt.Errorf(errUnexpectedStatus, resp.Status)
- }
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", fmt.Errorf(errReadResponse, err)
- }
- var result OIDCResponse
- err = json.Unmarshal(body, &result)
- if err != nil {
- return "", fmt.Errorf(errUnmarshalResponse, err)
- }
- if result.Token == "" {
- return "", errors.New(errTokenNotFound)
- }
- log.V(4).Info("Successfully exchanged OIDC token for Cloudsmith access token")
- return result.Token, nil
- }
- func parseSpec(specData []byte) (*genv1alpha1.CloudsmithAccessToken, error) {
- var spec genv1alpha1.CloudsmithAccessToken
- err := yaml.Unmarshal(specData, &spec)
- return &spec, err
- }
- func init() {
- genv1alpha1.Register(genv1alpha1.CloudsmithAccessTokenKind, &Generator{})
- }
|