github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/runtime/kubernetes/kubernetes.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/runtime/kubernetes/kubernetes.go 14 15 // Package kubernetes implements kubernetes micro runtime 16 package kubernetes 17 18 import ( 19 "fmt" 20 "sync" 21 "time" 22 23 "github.com/tickoalcantara12/micro/v3/service/logger" 24 "github.com/tickoalcantara12/micro/v3/service/runtime" 25 "github.com/tickoalcantara12/micro/v3/service/runtime/kubernetes/api" 26 "github.com/tickoalcantara12/micro/v3/service/runtime/kubernetes/client" 27 ) 28 29 var ( 30 DefaultServiceResources = &runtime.Resources{ 31 Mem: 200, 32 Disk: 2000, 33 // explicitly not doing CPU here 34 } 35 36 DefaultImage = "ghcr.io/micro/cells:v3" 37 ) 38 39 // action to take on runtime service 40 type action int 41 42 type kubernetes struct { 43 sync.Mutex 44 // options configure runtime 45 options runtime.Options 46 // client is kubernetes client 47 client client.Client 48 // namespaces which exist 49 namespaces []client.Namespace 50 } 51 52 // Init initializes runtime options 53 func (k *kubernetes) Init(opts ...runtime.Option) error { 54 for _, o := range opts { 55 o(&k.options) 56 } 57 return nil 58 } 59 60 func (k *kubernetes) Logs(resource runtime.Resource, options ...runtime.LogsOption) (runtime.LogStream, error) { 61 // Handle the various different types of resources: 62 switch resource.Type() { 63 case runtime.TypeNamespace: 64 // noop (Namespace is not supported by *kubernetes.Logs()) 65 return nil, nil 66 case runtime.TypeNetworkPolicy: 67 // noop (NetworkPolicy is not supported by *kubernetes.Logs())) 68 return nil, nil 69 case runtime.TypeResourceQuota: 70 // noop (ResourceQuota is not supported by *kubernetes.Logs())) 71 return nil, nil 72 case runtime.TypeService: 73 // Assert the resource back into a *runtime.Service 74 s, ok := resource.(*runtime.Service) 75 if !ok { 76 return nil, runtime.ErrInvalidResource 77 } 78 79 klo := newLog(k.client, s.Name, s.Version, options...) 80 81 // if its not a stream then read the records, return and close 82 if !klo.options.Stream { 83 records, err := klo.Read() 84 if err != nil { 85 logger.Errorf("Failed to get logs for service '%v' from k8s: %v", s.Name, err) 86 return nil, err 87 } 88 89 // create a stream even though we're not streaming 90 kstream := &kubeStream{ 91 // make a stream buffer of size records 92 stream: make(chan runtime.Log, len(records)), 93 stop: make(chan bool), 94 } 95 96 // load the records 97 for _, record := range records { 98 kstream.stream <- record 99 } 100 101 // close the stream so it doesn't block 102 close(kstream.stream) 103 104 return kstream, nil 105 } 106 107 // otherwise stream the logs 108 stream, err := klo.Stream() 109 if err != nil { 110 return nil, err 111 } 112 113 return stream, nil 114 default: 115 return nil, runtime.ErrInvalidResource 116 } 117 } 118 119 type kubeStream struct { 120 // the k8s log stream 121 stream chan runtime.Log 122 // the stop chan 123 sync.Mutex 124 stop chan bool 125 err error 126 } 127 128 func (k *kubeStream) Error() error { 129 return k.err 130 } 131 132 func (k *kubeStream) Chan() chan runtime.Log { 133 return k.stream 134 } 135 136 func (k *kubeStream) Stop() error { 137 k.Lock() 138 defer k.Unlock() 139 140 select { 141 case <-k.stop: 142 return nil 143 default: 144 close(k.stop) 145 } 146 147 return nil 148 } 149 150 // Create a resource 151 func (k *kubernetes) Create(resource runtime.Resource, opts ...runtime.CreateOption) error { 152 k.Lock() 153 defer k.Unlock() 154 return k.create(resource, opts...) 155 } 156 157 func (k *kubernetes) create(resource runtime.Resource, opts ...runtime.CreateOption) error { 158 // parse the options 159 options := &runtime.CreateOptions{ 160 Type: k.options.Type, 161 Image: k.options.Image, 162 Namespace: client.DefaultNamespace, 163 } 164 for _, o := range opts { 165 o(options) 166 } 167 168 // Handle the various different types of resources: 169 switch resource.Type() { 170 case runtime.TypeNamespace: 171 // Assert the resource back into a *runtime.Namespace 172 namespace, ok := resource.(*runtime.Namespace) 173 if !ok { 174 return runtime.ErrInvalidResource 175 } 176 return k.createNamespace(namespace) 177 case runtime.TypeNetworkPolicy: 178 // Assert the resource back into a *runtime.NetworkPolicy 179 networkPolicy, ok := resource.(*runtime.NetworkPolicy) 180 if !ok { 181 return runtime.ErrInvalidResource 182 } 183 return k.createNetworkPolicy(networkPolicy) 184 case runtime.TypeResourceQuota: 185 // Assert the resource back into a *runtime.ResourceQuota 186 resourceQuota, ok := resource.(*runtime.ResourceQuota) 187 if !ok { 188 return runtime.ErrInvalidResource 189 } 190 return k.createResourceQuota(resourceQuota) 191 case runtime.TypeService: 192 // Assert the resource back into a *runtime.Service 193 s, ok := resource.(*runtime.Service) 194 if !ok { 195 return runtime.ErrInvalidResource 196 } 197 198 // default the service's source and version 199 if len(s.Source) == 0 { 200 s.Source = k.options.Source 201 } 202 if len(s.Version) == 0 { 203 s.Version = "latest" 204 } 205 206 // ensure the namespace exists 207 if err := k.ensureNamepaceExists(options.Namespace); err != nil { 208 return nil 209 } 210 211 // create a secret for the deployment 212 if err := k.createCredentials(s, options); err != nil { 213 return err 214 } 215 216 // create some default resource requests 217 if options.Resources == nil && options.Namespace != "micro" { 218 options.Resources = DefaultServiceResources 219 } 220 221 if len(options.Image) == 0 { 222 options.Image = DefaultImage 223 } 224 225 // create the deployment and set the runtime class name if provided 226 dep := client.NewDeployment(s, options) 227 if rcn := getRuntimeClassName(k.options.Context); len(rcn) > 0 { 228 dep.Value.(*client.Deployment).Spec.Template.PodSpec.RuntimeClassName = rcn 229 logger.Infof("Setting runtime class name to %v", rcn) 230 } 231 232 // create the deployment 233 if err := k.client.Create(dep, client.CreateNamespace(options.Namespace)); err != nil { 234 if parseError(err).Reason == "AlreadyExists" { 235 return runtime.ErrAlreadyExists 236 } 237 if logger.V(logger.ErrorLevel, logger.DefaultLogger) { 238 logger.Errorf("Runtime failed to create deployment: %v", err) 239 } 240 return err 241 } 242 243 // create the service, one could already exist for another version so ignore ErrAlreadyExists 244 if err := k.client.Create(client.NewService(s, options), client.CreateNamespace(options.Namespace)); err != nil { 245 if parseError(err).Reason == "AlreadyExists" { 246 return nil 247 } 248 if logger.V(logger.ErrorLevel, logger.DefaultLogger) { 249 logger.Errorf("Runtime failed to create service: %v", err) 250 } 251 return err 252 } 253 254 return nil 255 default: 256 return runtime.ErrInvalidResource 257 } 258 } 259 260 // Read returns all instances of given service 261 func (k *kubernetes) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error) { 262 k.Lock() 263 defer k.Unlock() 264 265 // parse the options 266 options := runtime.ReadOptions{ 267 Namespace: client.DefaultNamespace, 268 } 269 for _, o := range opts { 270 o(&options) 271 } 272 273 // construct the query 274 labels := map[string]string{} 275 if len(options.Service) > 0 { 276 labels["name"] = client.Format(options.Service) 277 } 278 if len(options.Version) > 0 { 279 labels["version"] = client.Format(options.Version) 280 } 281 if len(options.Type) > 0 { 282 labels["micro"] = client.Format(options.Type) 283 } 284 285 // lookup all the serivces which match this query, if one service has two different versions, 286 // they'll be returned as two seperate resullts 287 return k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels)) 288 } 289 290 // Update a resource in place 291 func (k *kubernetes) Update(resource runtime.Resource, opts ...runtime.UpdateOption) error { 292 k.Lock() 293 defer k.Unlock() 294 295 // parse the options 296 options := runtime.UpdateOptions{ 297 Namespace: client.DefaultNamespace, 298 } 299 for _, o := range opts { 300 o(&options) 301 } 302 303 // Handle the various different types of resources: 304 switch resource.Type() { 305 case runtime.TypeNamespace: 306 // noop (Namespace is not supported by *kubernetes.Update()) 307 return nil 308 case runtime.TypeNetworkPolicy: 309 // Assert the resource back into a *runtime.NetworkPolicy 310 networkPolicy, ok := resource.(*runtime.NetworkPolicy) 311 if !ok { 312 return runtime.ErrInvalidResource 313 } 314 return k.updateNetworkPolicy(networkPolicy) 315 case runtime.TypeResourceQuota: 316 // Assert the resource back into a *runtime.ResourceQuota 317 resourceQuota, ok := resource.(*runtime.ResourceQuota) 318 if !ok { 319 return runtime.ErrInvalidResource 320 } 321 return k.updateResourceQuota(resourceQuota) 322 case runtime.TypeService: 323 324 // Assert the resource back into a *runtime.Service 325 s, ok := resource.(*runtime.Service) 326 if !ok { 327 return runtime.ErrInvalidResource 328 } 329 330 // construct the query 331 labels := map[string]string{} 332 if len(s.Name) > 0 { 333 labels["name"] = client.Format(s.Name) 334 } 335 if len(s.Version) > 0 { 336 labels["version"] = client.Format(s.Version) 337 } 338 339 // get the existing deployments 340 depList := new(client.DeploymentList) 341 d := &client.Resource{ 342 Kind: "deployment", 343 Value: depList, 344 } 345 depOpts := []client.GetOption{ 346 client.GetNamespace(options.Namespace), 347 client.GetLabels(labels), 348 } 349 if err := k.client.Get(d, depOpts...); err != nil { 350 return err 351 } else if len(depList.Items) == 0 { 352 return runtime.ErrNotFound 353 } 354 355 // update the deployments which match the query 356 for _, dep := range depList.Items { 357 // the service wan't created by the k8s runtime 358 if dep.Metadata == nil || dep.Metadata.Annotations == nil { 359 continue 360 } 361 362 // update metadata 363 for k, v := range s.Metadata { 364 dep.Metadata.Annotations[k] = v 365 } 366 367 if rcn := getRuntimeClassName(k.options.Context); len(rcn) > 0 { 368 dep.Spec.Template.PodSpec.RuntimeClassName = rcn 369 logger.Infof("Setting runtime class name to %v", rcn) 370 } 371 372 // update build time annotation 373 dep.Spec.Template.Metadata.Annotations["updated"] = fmt.Sprintf("%d", time.Now().Unix()) 374 375 // set num instances (there is currently no way to set to 0 376 if options.Instances > 0 { 377 dep.Spec.Replicas = int(options.Instances) 378 } 379 380 // update the deployment 381 res := &client.Resource{ 382 Kind: "deployment", 383 Name: resourceName(s), 384 Value: &dep, 385 } 386 if err := k.client.Update(res, client.UpdateNamespace(options.Namespace)); err != nil { 387 if logger.V(logger.ErrorLevel, logger.DefaultLogger) { 388 logger.Errorf("Runtime failed to update deployment: %v", err) 389 } 390 return err 391 } 392 } 393 394 return nil 395 default: 396 return runtime.ErrInvalidResource 397 } 398 } 399 400 // Delete removes a resource 401 func (k *kubernetes) Delete(resource runtime.Resource, opts ...runtime.DeleteOption) error { 402 k.Lock() 403 defer k.Unlock() 404 405 options := runtime.DeleteOptions{ 406 Namespace: client.DefaultNamespace, 407 } 408 for _, o := range opts { 409 o(&options) 410 } 411 412 // Handle the various different types of resources: 413 switch resource.Type() { 414 case runtime.TypeNamespace: 415 // Assert the resource back into a *runtime.Namespace 416 namespace, ok := resource.(*runtime.Namespace) 417 if !ok { 418 return runtime.ErrInvalidResource 419 } 420 return k.deleteNamespace(namespace) 421 case runtime.TypeNetworkPolicy: 422 // Assert the resource back into a *runtime.NetworkPolicy 423 networkPolicy, ok := resource.(*runtime.NetworkPolicy) 424 if !ok { 425 return runtime.ErrInvalidResource 426 } 427 return k.deleteNetworkPolicy(networkPolicy) 428 case runtime.TypeResourceQuota: 429 // Assert the resource back into a *runtime.ResourceQuota 430 resourceQuota, ok := resource.(*runtime.ResourceQuota) 431 if !ok { 432 return runtime.ErrInvalidResource 433 } 434 return k.deleteResourceQuota(resourceQuota) 435 case runtime.TypeService: 436 437 // Assert the resource back into a *runtime.Service 438 s, ok := resource.(*runtime.Service) 439 if !ok { 440 return runtime.ErrInvalidResource 441 } 442 443 // delete the deployment 444 dep := client.NewDeployment(s, &runtime.CreateOptions{ 445 Type: k.options.Type, 446 Namespace: options.Namespace, 447 }) 448 if err := k.client.Delete(dep, client.DeleteNamespace(options.Namespace)); err != nil { 449 if err == api.ErrNotFound { 450 return runtime.ErrNotFound 451 } 452 if logger.V(logger.ErrorLevel, logger.DefaultLogger) { 453 logger.Errorf("Runtime failed to delete deployment: %v", err) 454 } 455 return err 456 } 457 458 // delete the credentials 459 if err := k.deleteCredentials(s, &runtime.CreateOptions{Namespace: options.Namespace}); err != nil { 460 return err 461 } 462 463 // if there are more deployments for this service, then don't delete it 464 labels := map[string]string{} 465 if len(s.Name) > 0 { 466 labels["name"] = client.Format(s.Name) 467 } 468 469 // get the existing services. todo: refactor to just get the deployments 470 services, err := k.getServices(client.GetNamespace(options.Namespace), client.GetLabels(labels)) 471 if err != nil || len(services) > 0 { 472 return err 473 } 474 475 // delete the service 476 srv := client.NewService(s, &runtime.CreateOptions{ 477 Type: k.options.Type, 478 Namespace: options.Namespace, 479 }) 480 if err := k.client.Delete(srv, client.DeleteNamespace(options.Namespace)); err != nil { 481 if logger.V(logger.ErrorLevel, logger.DefaultLogger) { 482 logger.Errorf("Runtime failed to delete service: %v", err) 483 } 484 return err 485 } 486 487 return nil 488 default: 489 return runtime.ErrInvalidResource 490 } 491 } 492 493 // Start starts the runtime 494 func (k *kubernetes) Start() error { 495 return nil 496 } 497 498 // Stop shuts down the runtime 499 func (k *kubernetes) Stop() error { 500 return nil 501 } 502 503 // String implements stringer interface 504 func (k *kubernetes) String() string { 505 return "kubernetes" 506 } 507 508 // NewRuntime creates new kubernetes runtime 509 func NewRuntime(opts ...runtime.Option) runtime.Runtime { 510 // get default options 511 options := runtime.Options{ 512 // Create labels with type "micro": "service" 513 Type: "service", 514 } 515 516 // apply requested options 517 for _, o := range opts { 518 o(&options) 519 } 520 521 // kubernetes client 522 client := client.NewClusterClient() 523 524 return &kubernetes{ 525 options: options, 526 client: client, 527 } 528 }