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 }