github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/runtime/kubernetes/client/client.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); 2 // you may not use this file except in compliance with the License. 3 // You may obtain a copy of the License at 4 // 5 // https://www.apache.org/licenses/LICENSE-2.0 6 // 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 // Original source: github.com/micro/go-micro/v3/util/kubernetes/client/client.go 14 15 // Package client provides an implementation of a restricted subset of kubernetes API client 16 package client 17 18 import ( 19 "bytes" 20 "crypto/tls" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "os" 27 "path" 28 "regexp" 29 "strconv" 30 "strings" 31 32 "github.com/tickoalcantara12/micro/v3/service/logger" 33 "github.com/tickoalcantara12/micro/v3/service/runtime" 34 "github.com/tickoalcantara12/micro/v3/service/runtime/kubernetes/api" 35 ) 36 37 var ( 38 // path to kubernetes service account token 39 serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount" 40 // ErrReadNamespace is returned when the names could not be read from service account 41 ErrReadNamespace = errors.New("Could not read namespace from service account secret") 42 // DefaultImage is default micro image 43 DefaultImage = "ghcr.io/micro/cells:v3" 44 // DefaultNamespace is the default k8s namespace 45 DefaultNamespace = "default" 46 // DefaultPort to expose on a service 47 DefaultPort = 8080 48 ) 49 50 // Client ... 51 type client struct { 52 opts *api.Options 53 } 54 55 // Kubernetes client 56 type Client interface { 57 // Create creates new API resource 58 Create(*Resource, ...CreateOption) error 59 // Get queries API resources 60 Get(*Resource, ...GetOption) error 61 // Update patches existing API object 62 Update(*Resource, ...UpdateOption) error 63 // Delete deletes API resource 64 Delete(*Resource, ...DeleteOption) error 65 // List lists API resources 66 List(*Resource, ...ListOption) error 67 // Log gets log for a pod 68 Log(*Resource, ...LogOption) (io.ReadCloser, error) 69 // Watch for events 70 Watch(*Resource, ...WatchOption) (Watcher, error) 71 } 72 73 // Create creates new API object 74 func (c *client) Create(r *Resource, opts ...CreateOption) error { 75 options := CreateOptions{ 76 Namespace: c.opts.Namespace, 77 } 78 for _, o := range opts { 79 o(&options) 80 } 81 82 b := new(bytes.Buffer) 83 if err := renderTemplate(r.Kind, b, r.Value); err != nil { 84 return err 85 } 86 87 return api.NewRequest(c.opts). 88 Post(). 89 SetHeader("Content-Type", "application/yaml"). 90 Namespace(options.Namespace). 91 Resource(r.Kind). 92 Body(b). 93 Do(). 94 Error() 95 } 96 97 var ( 98 nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+") 99 ) 100 101 // Get queries API objects and stores the result in r 102 func (c *client) Get(r *Resource, opts ...GetOption) error { 103 options := GetOptions{ 104 Namespace: c.opts.Namespace, 105 } 106 for _, o := range opts { 107 o(&options) 108 } 109 110 return api.NewRequest(c.opts). 111 Get(). 112 Resource(r.Kind). 113 Namespace(options.Namespace). 114 Params(&api.Params{LabelSelector: options.Labels}). 115 Do(). 116 Into(r.Value) 117 } 118 119 // Log returns logs for a pod 120 func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) { 121 options := LogOptions{ 122 Namespace: c.opts.Namespace, 123 } 124 for _, o := range opts { 125 o(&options) 126 } 127 128 req := api.NewRequest(c.opts). 129 Get(). 130 Resource(r.Kind). 131 SubResource("log"). 132 Name(r.Name). 133 Namespace(options.Namespace) 134 135 if options.Params != nil { 136 req.Params(&api.Params{Additional: options.Params}) 137 } 138 139 resp, err := req.Raw() 140 if err != nil { 141 return nil, err 142 } 143 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 144 resp.Body.Close() 145 return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status) 146 } 147 return resp.Body, nil 148 } 149 150 // Update updates API object 151 func (c *client) Update(r *Resource, opts ...UpdateOption) error { 152 options := UpdateOptions{ 153 Namespace: c.opts.Namespace, 154 } 155 for _, o := range opts { 156 o(&options) 157 } 158 159 req := api.NewRequest(c.opts). 160 Patch(). 161 SetHeader("Content-Type", "application/strategic-merge-patch+json"). 162 Resource(r.Kind). 163 Name(r.Name). 164 Namespace(options.Namespace) 165 166 switch r.Kind { 167 case "service": 168 req.Body(r.Value.(*Service)) 169 case "deployment": 170 req.Body(r.Value.(*Deployment)) 171 case "pod": 172 req.Body(r.Value.(*Pod)) 173 case "networkpolicy", "networkpolicies": 174 req.Body(r.Value.(*NetworkPolicy)) 175 case "resourcequota": 176 req.Body(r.Value.(*ResourceQuota)) 177 default: 178 return errors.New("unsupported resource") 179 } 180 return req.Do().Error() 181 } 182 183 // Delete removes API object 184 func (c *client) Delete(r *Resource, opts ...DeleteOption) error { 185 options := DeleteOptions{ 186 Namespace: c.opts.Namespace, 187 } 188 for _, o := range opts { 189 o(&options) 190 } 191 192 return api.NewRequest(c.opts). 193 Delete(). 194 Resource(r.Kind). 195 Name(r.Name). 196 Namespace(options.Namespace). 197 Do(). 198 Error() 199 } 200 201 // List lists API objects and stores the result in r 202 func (c *client) List(r *Resource, opts ...ListOption) error { 203 options := ListOptions{ 204 Namespace: c.opts.Namespace, 205 } 206 for _, o := range opts { 207 o(&options) 208 } 209 210 return c.Get(r, GetNamespace(options.Namespace)) 211 } 212 213 // Watch returns an event stream 214 func (c *client) Watch(r *Resource, opts ...WatchOption) (Watcher, error) { 215 options := WatchOptions{ 216 Namespace: c.opts.Namespace, 217 } 218 for _, o := range opts { 219 o(&options) 220 } 221 222 // set the watch param 223 params := &api.Params{Additional: map[string]string{ 224 "watch": "true", 225 }} 226 227 // get options params 228 if options.Params != nil { 229 for k, v := range options.Params { 230 params.Additional[k] = v 231 } 232 } 233 234 req := api.NewRequest(c.opts). 235 Get(). 236 Resource(r.Kind). 237 Name(r.Name). 238 Namespace(options.Namespace). 239 Params(params) 240 241 return newWatcher(req) 242 } 243 244 // NewService returns default micro kubernetes service definition 245 func NewService(s *runtime.Service, opts *runtime.CreateOptions) *Resource { 246 labels := map[string]string{ 247 "name": Format(s.Name), 248 "version": Format(s.Version), 249 "micro": Format(opts.Type), 250 } 251 252 metadata := &Metadata{ 253 Name: Format(s.Name), 254 Namespace: Format(opts.Namespace), 255 Version: Format(s.Version), 256 Labels: labels, 257 } 258 259 port := DefaultPort 260 if len(opts.Port) > 0 { 261 port, _ = strconv.Atoi(opts.Port) 262 } 263 264 return &Resource{ 265 Kind: "service", 266 Name: metadata.Name, 267 Value: &Service{ 268 Metadata: metadata, 269 Spec: &ServiceSpec{ 270 Type: "ClusterIP", 271 Selector: labels, 272 Ports: []ServicePort{{ 273 "service-port", port, "", 274 }}, 275 }, 276 }, 277 } 278 } 279 280 // NewDeployment returns default micro kubernetes deployment definition 281 func NewDeployment(s *runtime.Service, opts *runtime.CreateOptions) *Resource { 282 labels := map[string]string{ 283 "name": Format(s.Name), 284 "version": Format(s.Version), 285 "micro": Format(opts.Type), 286 } 287 288 // attach our values to the deployment; name, version, source 289 annotations := map[string]string{ 290 "name": s.Name, 291 "version": s.Version, 292 "source": s.Source, 293 } 294 for k, v := range s.Metadata { 295 annotations[k] = v 296 } 297 298 // construct the metadata for the deployment 299 metadata := &Metadata{ 300 Name: fmt.Sprintf("%v-%v", Format(s.Name), Format(s.Version)), 301 Namespace: Format(opts.Namespace), 302 Version: Format(s.Version), 303 Labels: labels, 304 Annotations: annotations, 305 } 306 307 // set the image 308 image := opts.Image 309 if len(image) == 0 { 310 image = DefaultImage 311 } 312 313 // pass the env vars 314 env := make([]EnvVar, 0, len(opts.Env)) 315 for _, evar := range opts.Env { 316 if comps := strings.Split(evar, "="); len(comps) == 2 { 317 env = append(env, EnvVar{Name: comps[0], Value: comps[1]}) 318 } 319 } 320 321 // pass the secrets 322 for key := range opts.Secrets { 323 env = append(env, EnvVar{ 324 Name: key, 325 ValueFrom: &EnvVarSource{ 326 SecretKeyRef: &SecretKeySelector{ 327 Name: metadata.Name, 328 Key: key, 329 }, 330 }, 331 }) 332 } 333 334 // parse resource limits 335 var resReqs *ResourceRequirements 336 if opts.Resources != nil { 337 resReqs = &ResourceRequirements{Limits: &ResourceLimits{}} 338 339 if opts.Resources.CPU > 0 { 340 resReqs.Limits.CPU = fmt.Sprintf("%vm", opts.Resources.CPU) 341 } 342 if opts.Resources.Mem > 0 { 343 resReqs.Limits.Memory = fmt.Sprintf("%vMi", opts.Resources.Mem) 344 } 345 if opts.Resources.Disk > 0 { 346 resReqs.Limits.EphemeralStorage = fmt.Sprintf("%vMi", opts.Resources.Disk) 347 } 348 } 349 350 // parse the port option 351 port := DefaultPort 352 if len(opts.Port) > 0 { 353 port, _ = strconv.Atoi(opts.Port) 354 } 355 356 // set the number of replicas to run 357 replicas := 1 358 if opts.Instances > 1 { 359 replicas = int(opts.Instances) 360 } 361 362 return &Resource{ 363 Kind: "deployment", 364 Name: metadata.Name, 365 Value: &Deployment{ 366 Metadata: metadata, 367 Spec: &DeploymentSpec{ 368 Replicas: replicas, 369 Selector: &LabelSelector{ 370 MatchLabels: labels, 371 }, 372 Template: &Template{ 373 Metadata: metadata, 374 PodSpec: &PodSpec{ 375 ServiceAccountName: opts.ServiceAccount, 376 Containers: []Container{{ 377 Name: Format(s.Name), 378 Image: image, 379 Env: env, 380 Command: opts.Command, 381 Args: opts.Args, 382 Ports: []ContainerPort{{ 383 Name: "service-port", 384 ContainerPort: port, 385 }}, 386 ReadinessProbe: &Probe{ 387 TCPSocket: &TCPSocketAction{ 388 Port: port, 389 }, 390 PeriodSeconds: 10, 391 InitialDelaySeconds: 10, 392 }, 393 Resources: resReqs, 394 }}, 395 }, 396 }, 397 }, 398 }, 399 } 400 } 401 402 // NewLocalClient returns a client that can be used with `kubectl proxy` 403 func NewLocalClient(hosts ...string) *client { 404 if len(hosts) == 0 { 405 hosts[0] = "http://localhost:8001" 406 } 407 return &client{ 408 opts: &api.Options{ 409 Client: http.DefaultClient, 410 Host: hosts[0], 411 Namespace: "default", 412 }, 413 } 414 } 415 416 // NewClusterClient creates a Kubernetes client for use from within a k8s pod. 417 func NewClusterClient() *client { 418 host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT") 419 420 s, err := os.Stat(serviceAccountPath) 421 if err != nil { 422 logger.Fatal(err) 423 } 424 if s == nil || !s.IsDir() { 425 logger.Fatal(errors.New("service account not found")) 426 } 427 428 token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token")) 429 if err != nil { 430 logger.Fatal(err) 431 } 432 t := string(token) 433 434 crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt")) 435 if err != nil { 436 logger.Fatal(err) 437 } 438 439 c := &http.Client{ 440 Transport: &http.Transport{ 441 TLSClientConfig: &tls.Config{ 442 RootCAs: crt, 443 }, 444 DisableCompression: true, 445 }, 446 } 447 448 return &client{ 449 opts: &api.Options{ 450 Client: c, 451 Host: host, 452 BearerToken: &t, 453 Namespace: DefaultNamespace, 454 }, 455 } 456 } 457 458 // NewNetworkPolicy returns a network policy allowing ingress from the given labels 459 func NewNetworkPolicy(name, namespace string, allowedLabels map[string]string) *NetworkPolicy { 460 np := &NetworkPolicy{ 461 Metadata: &Metadata{ 462 Name: name, 463 Namespace: namespace, 464 }, 465 Spec: &NetworkPolicySpec{ 466 Ingress: []NetworkPolicyRule{ 467 { 468 From: []IngressRuleSelector{ 469 { // allow pods in this namespace to talk to each other 470 PodSelector: &Selector{}, 471 }, 472 }, 473 }, 474 { 475 From: []IngressRuleSelector{ 476 { 477 NamespaceSelector: &Selector{ 478 MatchLabels: allowedLabels, 479 }, 480 }, 481 }, 482 }, 483 }, 484 PodSelector: &Selector{}, 485 PolicyTypes: []string{"Ingress"}, 486 }, 487 } 488 return np 489 490 } 491 492 func NewResourceQuota(resourceQuota *runtime.ResourceQuota) *ResourceQuota { 493 rq := &ResourceQuota{ 494 Metadata: &Metadata{ 495 Name: resourceQuota.Name, 496 Namespace: resourceQuota.Namespace, 497 }, 498 Spec: &ResourceQuotaSpec{ 499 Hard: &ResourceQuotaSpecs{}, 500 }, 501 } 502 if resourceQuota.Limits != nil { 503 if resourceQuota.Limits.CPU > 0 { 504 rq.Spec.Hard.LimitsCPU = fmt.Sprintf("%dm", resourceQuota.Limits.CPU) 505 } 506 if resourceQuota.Limits.Disk > 0 { 507 rq.Spec.Hard.LimitsEphemeralStorage = fmt.Sprintf("%dMi", resourceQuota.Limits.Disk) 508 } 509 if resourceQuota.Limits.Mem > 0 { 510 rq.Spec.Hard.LimitsMemory = fmt.Sprintf("%dMi", resourceQuota.Limits.Mem) 511 } 512 } 513 if resourceQuota.Requests != nil { 514 if resourceQuota.Requests.CPU > 0 { 515 rq.Spec.Hard.RequestsCPU = fmt.Sprintf("%dm", resourceQuota.Requests.CPU) 516 } 517 if resourceQuota.Requests.Disk > 0 { 518 rq.Spec.Hard.RequestsEphemeralStorage = fmt.Sprintf("%dMi", resourceQuota.Requests.Disk) 519 } 520 if resourceQuota.Requests.Mem > 0 { 521 rq.Spec.Hard.RequestsMemory = fmt.Sprintf("%dMi", resourceQuota.Requests.Mem) 522 } 523 } 524 525 return rq 526 }