gitee.com/sasukebo/go-micro/v4@v4.7.1/util/kubernetes/client/client.go (about) 1 // Package client provides an implementation of a restricted subset of kubernetes API client 2 package client 3 4 import ( 5 "bytes" 6 "crypto/tls" 7 "errors" 8 "io" 9 "net/http" 10 "os" 11 "path" 12 "regexp" 13 "strings" 14 15 "gitee.com/sasukebo/go-micro/v4/logger" 16 "gitee.com/sasukebo/go-micro/v4/util/kubernetes/api" 17 ) 18 19 var ( 20 // path to kubernetes service account token 21 serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount" 22 // ErrReadNamespace is returned when the names could not be read from service account 23 ErrReadNamespace = errors.New("Could not read namespace from service account secret") 24 // DefaultImage is default micro image 25 DefaultImage = "micro/go-micro" 26 // DefaultNamespace is the default k8s namespace 27 DefaultNamespace = "default" 28 ) 29 30 // Client ... 31 type client struct { 32 opts *api.Options 33 } 34 35 // Kubernetes client 36 type Client interface { 37 // Create creates new API resource 38 Create(*Resource, ...CreateOption) error 39 // Get queries API resrouces 40 Get(*Resource, ...GetOption) error 41 // Update patches existing API object 42 Update(*Resource, ...UpdateOption) error 43 // Delete deletes API resource 44 Delete(*Resource, ...DeleteOption) error 45 // List lists API resources 46 List(*Resource, ...ListOption) error 47 // Log gets log for a pod 48 Log(*Resource, ...LogOption) (io.ReadCloser, error) 49 // Watch for events 50 Watch(*Resource, ...WatchOption) (Watcher, error) 51 } 52 53 // Create creates new API object 54 func (c *client) Create(r *Resource, opts ...CreateOption) error { 55 options := CreateOptions{ 56 Namespace: c.opts.Namespace, 57 } 58 for _, o := range opts { 59 o(&options) 60 } 61 62 b := new(bytes.Buffer) 63 if err := renderTemplate(r.Kind, b, r.Value); err != nil { 64 return err 65 } 66 resp := api.NewRequest(c.opts). 67 Post(). 68 SetHeader("Content-Type", "application/yaml"). 69 Namespace(options.Namespace). 70 Resource(r.Kind). 71 Body(b). 72 Do() 73 resp.Close() 74 return resp.Error() 75 } 76 77 var ( 78 nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+") 79 ) 80 81 // SerializeResourceName removes all spacial chars from a string so it 82 // can be used as a k8s resource name 83 func SerializeResourceName(ns string) string { 84 return nameRegex.ReplaceAllString(ns, "-") 85 } 86 87 // Get queries API objects and stores the result in r 88 func (c *client) Get(r *Resource, opts ...GetOption) error { 89 options := GetOptions{ 90 Namespace: c.opts.Namespace, 91 } 92 for _, o := range opts { 93 o(&options) 94 } 95 96 return api.NewRequest(c.opts). 97 Get(). 98 Resource(r.Kind). 99 Namespace(options.Namespace). 100 Params(&api.Params{LabelSelector: options.Labels}). 101 Do(). 102 Into(r.Value) 103 } 104 105 // Log returns logs for a pod 106 func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) { 107 options := LogOptions{ 108 Namespace: c.opts.Namespace, 109 } 110 for _, o := range opts { 111 o(&options) 112 } 113 114 req := api.NewRequest(c.opts). 115 Get(). 116 Resource(r.Kind). 117 SubResource("log"). 118 Name(r.Name). 119 Namespace(options.Namespace) 120 121 if options.Params != nil { 122 req.Params(&api.Params{Additional: options.Params}) 123 } 124 125 resp, err := req.Raw() 126 if err != nil { 127 return nil, err 128 } 129 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 130 resp.Body.Close() 131 return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status) 132 } 133 return resp.Body, nil 134 } 135 136 // Update updates API object 137 func (c *client) Update(r *Resource, opts ...UpdateOption) error { 138 options := UpdateOptions{ 139 Namespace: c.opts.Namespace, 140 } 141 for _, o := range opts { 142 o(&options) 143 } 144 145 req := api.NewRequest(c.opts). 146 Patch(). 147 SetHeader("Content-Type", "application/strategic-merge-patch+json"). 148 Resource(r.Kind). 149 Name(r.Name). 150 Namespace(options.Namespace) 151 152 switch r.Kind { 153 case "service": 154 req.Body(r.Value.(*Service)) 155 case "deployment": 156 req.Body(r.Value.(*Deployment)) 157 case "pod": 158 req.Body(r.Value.(*Pod)) 159 default: 160 return errors.New("unsupported resource") 161 } 162 resp := req.Do() 163 resp.Close() 164 return resp.Error() 165 } 166 167 // Delete removes API object 168 func (c *client) Delete(r *Resource, opts ...DeleteOption) error { 169 options := DeleteOptions{ 170 Namespace: c.opts.Namespace, 171 } 172 for _, o := range opts { 173 o(&options) 174 } 175 resp := api.NewRequest(c.opts). 176 Delete(). 177 Resource(r.Kind). 178 Name(r.Name). 179 Namespace(options.Namespace). 180 Do() 181 resp.Close() 182 return resp.Error() 183 } 184 185 // List lists API objects and stores the result in r 186 func (c *client) List(r *Resource, opts ...ListOption) error { 187 options := ListOptions{ 188 Namespace: c.opts.Namespace, 189 } 190 for _, o := range opts { 191 o(&options) 192 } 193 194 return c.Get(r, GetNamespace(options.Namespace)) 195 } 196 197 // Watch returns an event stream 198 func (c *client) Watch(r *Resource, opts ...WatchOption) (Watcher, error) { 199 options := WatchOptions{ 200 Namespace: c.opts.Namespace, 201 } 202 for _, o := range opts { 203 o(&options) 204 } 205 206 // set the watch param 207 params := &api.Params{Additional: map[string]string{ 208 "watch": "true", 209 }} 210 211 // get options params 212 if options.Params != nil { 213 for k, v := range options.Params { 214 params.Additional[k] = v 215 } 216 } 217 218 req := api.NewRequest(c.opts). 219 Get(). 220 Resource(r.Kind). 221 Name(r.Name). 222 Namespace(options.Namespace). 223 Params(params) 224 225 return newWatcher(req) 226 } 227 228 // NewService returns default micro kubernetes service definition 229 func NewService(name, version, typ, namespace string) *Service { 230 if logger.V(logger.TraceLevel, logger.DefaultLogger) { 231 logger.Tracef("kubernetes default service: name: %s, version: %s", name, version) 232 } 233 234 Labels := map[string]string{ 235 "name": name, 236 "version": version, 237 "micro": typ, 238 } 239 240 svcName := name 241 if len(version) > 0 { 242 // API service object name joins name and version over "-" 243 svcName = strings.Join([]string{name, version}, "-") 244 } 245 246 if len(namespace) == 0 { 247 namespace = DefaultNamespace 248 } 249 250 Metadata := &Metadata{ 251 Name: svcName, 252 Namespace: SerializeResourceName(namespace), 253 Version: version, 254 Labels: Labels, 255 } 256 257 Spec := &ServiceSpec{ 258 Type: "ClusterIP", 259 Selector: Labels, 260 Ports: []ServicePort{{ 261 "service-port", 8080, "", 262 }}, 263 } 264 265 return &Service{ 266 Metadata: Metadata, 267 Spec: Spec, 268 } 269 } 270 271 // NewService returns default micro kubernetes deployment definition 272 func NewDeployment(name, version, typ, namespace string) *Deployment { 273 if logger.V(logger.TraceLevel, logger.DefaultLogger) { 274 logger.Tracef("kubernetes default deployment: name: %s, version: %s", name, version) 275 } 276 277 Labels := map[string]string{ 278 "name": name, 279 "version": version, 280 "micro": typ, 281 } 282 283 depName := name 284 if len(version) > 0 { 285 // API deployment object name joins name and version over "-" 286 depName = strings.Join([]string{name, version}, "-") 287 } 288 289 if len(namespace) == 0 { 290 namespace = DefaultNamespace 291 } 292 293 Metadata := &Metadata{ 294 Name: depName, 295 Namespace: SerializeResourceName(namespace), 296 Version: version, 297 Labels: Labels, 298 Annotations: map[string]string{}, 299 } 300 301 // enable go modules by default 302 env := EnvVar{ 303 Name: "GO111MODULE", 304 Value: "on", 305 } 306 307 Spec := &DeploymentSpec{ 308 Replicas: 1, 309 Selector: &LabelSelector{ 310 MatchLabels: Labels, 311 }, 312 Template: &Template{ 313 Metadata: Metadata, 314 PodSpec: &PodSpec{ 315 ServiceAccountName: namespace, 316 Containers: []Container{{ 317 Name: name, 318 Image: DefaultImage, 319 Env: []EnvVar{env}, 320 Command: []string{"go", "run", "."}, 321 Ports: []ContainerPort{{ 322 Name: "service-port", 323 ContainerPort: 8080, 324 }}, 325 }}, 326 }, 327 }, 328 } 329 330 return &Deployment{ 331 Metadata: Metadata, 332 Spec: Spec, 333 } 334 } 335 336 // NewLocalClient returns a client that can be used with `kubectl proxy` 337 func NewLocalClient(hosts ...string) *client { 338 if len(hosts) == 0 { 339 hosts[0] = "http://localhost:8001" 340 } 341 return &client{ 342 opts: &api.Options{ 343 Client: http.DefaultClient, 344 Host: hosts[0], 345 Namespace: "default", 346 }, 347 } 348 } 349 350 // NewClusterClient creates a Kubernetes client for use from within a k8s pod. 351 func NewClusterClient() *client { 352 host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT") 353 354 s, err := os.Stat(serviceAccountPath) 355 if err != nil { 356 logger.Fatal(err) 357 } 358 if s == nil || !s.IsDir() { 359 logger.Fatal(errors.New("service account not found")) 360 } 361 362 token, err := os.ReadFile(path.Join(serviceAccountPath, "token")) 363 if err != nil { 364 logger.Fatal(err) 365 } 366 t := string(token) 367 368 crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt")) 369 if err != nil { 370 logger.Fatal(err) 371 } 372 373 c := &http.Client{ 374 Transport: &http.Transport{ 375 TLSClientConfig: &tls.Config{ 376 RootCAs: crt, 377 }, 378 DisableCompression: true, 379 }, 380 } 381 382 return &client{ 383 opts: &api.Options{ 384 Client: c, 385 Host: host, 386 BearerToken: &t, 387 Namespace: DefaultNamespace, 388 }, 389 } 390 }