github.com/annwntech/go-micro/v2@v2.9.5/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 "io/ioutil" 10 "net/http" 11 "os" 12 "path" 13 "regexp" 14 "strings" 15 16 "github.com/annwntech/go-micro/v2/logger" 17 "github.com/annwntech/go-micro/v2/util/kubernetes/api" 18 ) 19 20 var ( 21 // path to kubernetes service account token 22 serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount" 23 // ErrReadNamespace is returned when the names could not be read from service account 24 ErrReadNamespace = errors.New("Could not read namespace from service account secret") 25 // DefaultImage is default micro image 26 DefaultImage = "micro/go-micro" 27 // DefaultNamespace is the default k8s namespace 28 DefaultNamespace = "default" 29 ) 30 31 // Client ... 32 type client struct { 33 opts *api.Options 34 } 35 36 // Kubernetes client 37 type Client interface { 38 // Create creates new API resource 39 Create(*Resource, ...CreateOption) error 40 // Get queries API resrouces 41 Get(*Resource, ...GetOption) error 42 // Update patches existing API object 43 Update(*Resource, ...UpdateOption) error 44 // Delete deletes API resource 45 Delete(*Resource, ...DeleteOption) error 46 // List lists API resources 47 List(*Resource, ...ListOption) error 48 // Log gets log for a pod 49 Log(*Resource, ...LogOption) (io.ReadCloser, error) 50 // Watch for events 51 Watch(*Resource, ...WatchOption) (Watcher, error) 52 } 53 54 // Create creates new API object 55 func (c *client) Create(r *Resource, opts ...CreateOption) error { 56 options := CreateOptions{ 57 Namespace: c.opts.Namespace, 58 } 59 for _, o := range opts { 60 o(&options) 61 } 62 63 b := new(bytes.Buffer) 64 if err := renderTemplate(r.Kind, b, r.Value); err != nil { 65 return err 66 } 67 68 return api.NewRequest(c.opts). 69 Post(). 70 SetHeader("Content-Type", "application/yaml"). 71 Namespace(options.Namespace). 72 Resource(r.Kind). 73 Body(b). 74 Do(). 75 Error() 76 } 77 78 var ( 79 nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+") 80 ) 81 82 // SerializeResourceName removes all spacial chars from a string so it 83 // can be used as a k8s resource name 84 func SerializeResourceName(ns string) string { 85 return nameRegex.ReplaceAllString(ns, "-") 86 } 87 88 // Get queries API objects and stores the result in r 89 func (c *client) Get(r *Resource, opts ...GetOption) error { 90 options := GetOptions{ 91 Namespace: c.opts.Namespace, 92 } 93 for _, o := range opts { 94 o(&options) 95 } 96 97 return api.NewRequest(c.opts). 98 Get(). 99 Resource(r.Kind). 100 Namespace(options.Namespace). 101 Params(&api.Params{LabelSelector: options.Labels}). 102 Do(). 103 Into(r.Value) 104 } 105 106 // Log returns logs for a pod 107 func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) { 108 options := LogOptions{ 109 Namespace: c.opts.Namespace, 110 } 111 for _, o := range opts { 112 o(&options) 113 } 114 115 req := api.NewRequest(c.opts). 116 Get(). 117 Resource(r.Kind). 118 SubResource("log"). 119 Name(r.Name). 120 Namespace(options.Namespace) 121 122 if options.Params != nil { 123 req.Params(&api.Params{Additional: options.Params}) 124 } 125 126 resp, err := req.Raw() 127 if err != nil { 128 return nil, err 129 } 130 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 131 resp.Body.Close() 132 return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status) 133 } 134 return resp.Body, nil 135 } 136 137 // Update updates API object 138 func (c *client) Update(r *Resource, opts ...UpdateOption) error { 139 options := UpdateOptions{ 140 Namespace: c.opts.Namespace, 141 } 142 for _, o := range opts { 143 o(&options) 144 } 145 146 req := api.NewRequest(c.opts). 147 Patch(). 148 SetHeader("Content-Type", "application/strategic-merge-patch+json"). 149 Resource(r.Kind). 150 Name(r.Name). 151 Namespace(options.Namespace) 152 153 switch r.Kind { 154 case "service": 155 req.Body(r.Value.(*Service)) 156 case "deployment": 157 req.Body(r.Value.(*Deployment)) 158 case "pod": 159 req.Body(r.Value.(*Pod)) 160 default: 161 return errors.New("unsupported resource") 162 } 163 164 return req.Do().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 176 return api.NewRequest(c.opts). 177 Delete(). 178 Resource(r.Kind). 179 Name(r.Name). 180 Namespace(options.Namespace). 181 Do(). 182 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 := ioutil.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 }