github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/dns/operator_dns.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package dns
    19  
    20  import (
    21  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"net"
    28  	"net/http"
    29  	"net/url"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/golang-jwt/jwt/v4"
    35  	"github.com/minio/minio/internal/config"
    36  	xhttp "github.com/minio/minio/internal/http"
    37  )
    38  
    39  var (
    40  	defaultOperatorContextTimeout = 10 * time.Second
    41  	// ErrNotImplemented - Indicates the functionality which is not implemented
    42  	ErrNotImplemented = errors.New("The method is not implemented")
    43  )
    44  
    45  func (c *OperatorDNS) addAuthHeader(r *http.Request) error {
    46  	if c.username == "" || c.password == "" {
    47  		return nil
    48  	}
    49  
    50  	claims := &jwt.StandardClaims{
    51  		ExpiresAt: int64(15 * time.Minute),
    52  		Issuer:    c.username,
    53  		Subject:   config.EnvDNSWebhook,
    54  	}
    55  
    56  	token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
    57  	ss, err := token.SignedString([]byte(c.password))
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	r.Header.Set("Authorization", "Bearer "+ss)
    63  	return nil
    64  }
    65  
    66  func (c *OperatorDNS) endpoint(bucket string, delete bool) (string, error) {
    67  	u, err := url.Parse(c.Endpoint)
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	q := u.Query()
    72  	q.Add("bucket", bucket)
    73  	q.Add("delete", strconv.FormatBool(delete))
    74  	u.RawQuery = q.Encode()
    75  	return u.String(), nil
    76  }
    77  
    78  // Put - Adds DNS entries into operator webhook server
    79  func (c *OperatorDNS) Put(bucket string) error {
    80  	ctx, cancel := context.WithTimeout(context.Background(), defaultOperatorContextTimeout)
    81  	defer cancel()
    82  	e, err := c.endpoint(bucket, false)
    83  	if err != nil {
    84  		return newError(bucket, err)
    85  	}
    86  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, e, nil)
    87  	if err != nil {
    88  		return newError(bucket, err)
    89  	}
    90  	if err = c.addAuthHeader(req); err != nil {
    91  		return newError(bucket, err)
    92  	}
    93  
    94  	resp, err := c.httpClient.Do(req)
    95  	if err != nil {
    96  		if derr := c.Delete(bucket); derr != nil {
    97  			return newError(bucket, derr)
    98  		}
    99  		return err
   100  	}
   101  	defer xhttp.DrainBody(resp.Body)
   102  
   103  	if resp.StatusCode != http.StatusOK {
   104  		var errorStringBuilder strings.Builder
   105  		io.Copy(&errorStringBuilder, io.LimitReader(resp.Body, resp.ContentLength))
   106  		errorString := errorStringBuilder.String()
   107  		if resp.StatusCode == http.StatusConflict {
   108  			return ErrBucketConflict(Error{bucket, errors.New(errorString)})
   109  		}
   110  		return newError(bucket, fmt.Errorf("service create for bucket %s, failed with status %s, error %s", bucket, resp.Status, errorString))
   111  	}
   112  	return nil
   113  }
   114  
   115  func newError(bucket string, err error) error {
   116  	e := Error{bucket, err}
   117  	if strings.Contains(err.Error(), "invalid bucket name") {
   118  		return ErrInvalidBucketName(e)
   119  	}
   120  	return e
   121  }
   122  
   123  // Delete - Removes DNS entries added in Put().
   124  func (c *OperatorDNS) Delete(bucket string) error {
   125  	ctx, cancel := context.WithTimeout(context.Background(), defaultOperatorContextTimeout)
   126  	defer cancel()
   127  	e, err := c.endpoint(bucket, true)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, e, nil)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	if err = c.addAuthHeader(req); err != nil {
   136  		return err
   137  	}
   138  	resp, err := c.httpClient.Do(req)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	xhttp.DrainBody(resp.Body)
   143  	if resp.StatusCode != http.StatusOK {
   144  		return fmt.Errorf("request to delete the service for bucket %s, failed with status %s", bucket, resp.Status)
   145  	}
   146  	return nil
   147  }
   148  
   149  // DeleteRecord - Removes a specific DNS entry
   150  // No Op for Operator because operator deals on with bucket entries
   151  func (c *OperatorDNS) DeleteRecord(record SrvRecord) error {
   152  	return ErrNotImplemented
   153  }
   154  
   155  // Close closes the internal http client
   156  func (c *OperatorDNS) Close() error {
   157  	return nil
   158  }
   159  
   160  // List - Retrieves list of DNS entries for the domain.
   161  // This is a No Op for Operator because, there is no intent to enforce global
   162  // namespace at MinIO level with this DNS entry. The global namespace in
   163  // enforced by the Kubernetes Operator
   164  func (c *OperatorDNS) List() (srvRecords map[string][]SrvRecord, err error) {
   165  	return nil, ErrNotImplemented
   166  }
   167  
   168  // Get - Retrieves DNS records for a bucket.
   169  // This is a No Op for Operator because, there is no intent to enforce global
   170  // namespace at MinIO level with this DNS entry. The global namespace in
   171  // enforced by the Kubernetes Operator
   172  func (c *OperatorDNS) Get(bucket string) (srvRecords []SrvRecord, err error) {
   173  	return nil, ErrNotImplemented
   174  }
   175  
   176  // String stringer name for this implementation of dns.Store
   177  func (c *OperatorDNS) String() string {
   178  	return "webhookDNS"
   179  }
   180  
   181  // OperatorDNS - represents dns config for MinIO k8s operator.
   182  type OperatorDNS struct {
   183  	httpClient *http.Client
   184  	Endpoint   string
   185  	rootCAs    *x509.CertPool
   186  	username   string
   187  	password   string
   188  }
   189  
   190  // OperatorOption - functional options pattern style for OperatorDNS
   191  type OperatorOption func(*OperatorDNS)
   192  
   193  // Authentication - custom username and password for authenticating at the endpoint
   194  func Authentication(username, password string) OperatorOption {
   195  	return func(args *OperatorDNS) {
   196  		args.username = username
   197  		args.password = password
   198  	}
   199  }
   200  
   201  // RootCAs - add custom trust certs pool
   202  func RootCAs(certPool *x509.CertPool) OperatorOption {
   203  	return func(args *OperatorDNS) {
   204  		args.rootCAs = certPool
   205  	}
   206  }
   207  
   208  // NewOperatorDNS - initialize a new K8S Operator DNS set/unset values.
   209  func NewOperatorDNS(endpoint string, setters ...OperatorOption) (Store, error) {
   210  	if endpoint == "" {
   211  		return nil, errors.New("invalid argument")
   212  	}
   213  
   214  	args := &OperatorDNS{
   215  		Endpoint: endpoint,
   216  	}
   217  	for _, setter := range setters {
   218  		setter(args)
   219  	}
   220  	args.httpClient = &http.Client{
   221  		Transport: &http.Transport{
   222  			Proxy: http.ProxyFromEnvironment,
   223  			DialContext: (&net.Dialer{
   224  				Timeout:   3 * time.Second,
   225  				KeepAlive: 5 * time.Second,
   226  			}).DialContext,
   227  			ResponseHeaderTimeout: 3 * time.Second,
   228  			TLSHandshakeTimeout:   3 * time.Second,
   229  			ExpectContinueTimeout: 3 * time.Second,
   230  			TLSClientConfig: &tls.Config{
   231  				RootCAs: args.rootCAs,
   232  			},
   233  			// Go net/http automatically unzip if content-type is
   234  			// gzip disable this feature, as we are always interested
   235  			// in raw stream.
   236  			DisableCompression: true,
   237  		},
   238  	}
   239  	return args, nil
   240  }