diff --git a/.changelog/1938.txt b/.changelog/1938.txt new file mode 100644 index 00000000000..9d39b5549ec --- /dev/null +++ b/.changelog/1938.txt @@ -0,0 +1,3 @@ +```release-note:improvement +plugin/k8s: Update K8s Releaser to use SDK Resource Manager +``` diff --git a/builtin/k8s/plugin.pb.go b/builtin/k8s/plugin.pb.go index 45737bb47b8..831582eec93 100644 --- a/builtin/k8s/plugin.pb.go +++ b/builtin/k8s/plugin.pb.go @@ -90,8 +90,9 @@ type Release struct { unknownFields protoimpl.UnknownFields // service_name is the name of the service in Kubernetes - ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` - Url string `protobuf:"bytes,1,opt,name=Url,proto3" json:"Url,omitempty"` + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Url string `protobuf:"bytes,1,opt,name=Url,proto3" json:"Url,omitempty"` + ResourceState *anypb.Any `protobuf:"bytes,3,opt,name=resource_state,json=resourceState,proto3" json:"resource_state,omitempty"` } func (x *Release) Reset() { @@ -140,6 +141,13 @@ func (x *Release) GetUrl() string { return "" } +func (x *Release) GetResourceState() *anypb.Any { + if x != nil { + return x.ResourceState + } + return nil +} + // Resource contains the internal resource states. type Resource struct { state protoimpl.MessageState @@ -226,6 +234,53 @@ func (x *Resource_Deployment) GetName() string { return "" } +type Resource_Service struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *Resource_Service) Reset() { + *x = Resource_Service{} + if protoimpl.UnsafeEnabled { + mi := &file_waypoint_builtin_k8s_plugin_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Resource_Service) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Resource_Service) ProtoMessage() {} + +func (x *Resource_Service) ProtoReflect() protoreflect.Message { + mi := &file_waypoint_builtin_k8s_plugin_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Resource_Service.ProtoReflect.Descriptor instead. +func (*Resource_Service) Descriptor() ([]byte, []int) { + return file_waypoint_builtin_k8s_plugin_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *Resource_Service) GetName() string { + if x != nil { + return x.Name + } + return "" +} + var File_waypoint_builtin_k8s_plugin_proto protoreflect.FileDescriptor var file_waypoint_builtin_k8s_plugin_proto_rawDesc = []byte{ @@ -240,15 +295,21 @@ var file_waypoint_builtin_k8s_plugin_proto_rawDesc = []byte{ 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x22, 0x3e, 0x0a, 0x07, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x21, 0x0a, + 0x74, 0x65, 0x22, 0x7b, 0x0a, 0x07, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, - 0x72, 0x6c, 0x22, 0x2c, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x20, - 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x42, 0x16, 0x5a, 0x14, 0x77, 0x61, 0x79, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2f, 0x62, 0x75, 0x69, - 0x6c, 0x74, 0x69, 0x6e, 0x2f, 0x6b, 0x38, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6c, 0x12, 0x3b, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, + 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, + 0x4b, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x20, 0x0a, 0x0a, 0x44, + 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x1a, 0x1d, 0x0a, + 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x16, 0x5a, 0x14, + 0x77, 0x61, 0x79, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, + 0x2f, 0x6b, 0x38, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -263,21 +324,23 @@ func file_waypoint_builtin_k8s_plugin_proto_rawDescGZIP() []byte { return file_waypoint_builtin_k8s_plugin_proto_rawDescData } -var file_waypoint_builtin_k8s_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_waypoint_builtin_k8s_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_waypoint_builtin_k8s_plugin_proto_goTypes = []interface{}{ (*Deployment)(nil), // 0: k8s.Deployment (*Release)(nil), // 1: k8s.Release (*Resource)(nil), // 2: k8s.Resource (*Resource_Deployment)(nil), // 3: k8s.Resource.Deployment - (*anypb.Any)(nil), // 4: google.protobuf.Any + (*Resource_Service)(nil), // 4: k8s.Resource.Service + (*anypb.Any)(nil), // 5: google.protobuf.Any } var file_waypoint_builtin_k8s_plugin_proto_depIdxs = []int32{ - 4, // 0: k8s.Deployment.resource_state:type_name -> google.protobuf.Any - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 5, // 0: k8s.Deployment.resource_state:type_name -> google.protobuf.Any + 5, // 1: k8s.Release.resource_state:type_name -> google.protobuf.Any + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_waypoint_builtin_k8s_plugin_proto_init() } @@ -334,6 +397,18 @@ func file_waypoint_builtin_k8s_plugin_proto_init() { return nil } } + file_waypoint_builtin_k8s_plugin_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Resource_Service); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -341,7 +416,7 @@ func file_waypoint_builtin_k8s_plugin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_waypoint_builtin_k8s_plugin_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/builtin/k8s/plugin.proto b/builtin/k8s/plugin.proto index 098f848fa2c..af4ce18afd3 100644 --- a/builtin/k8s/plugin.proto +++ b/builtin/k8s/plugin.proto @@ -16,6 +16,7 @@ message Release { // service_name is the name of the service in Kubernetes string service_name = 2; string Url = 1; + google.protobuf.Any resource_state = 3; } // Resource contains the internal resource states. @@ -23,4 +24,7 @@ message Resource { message Deployment { string name = 1; } + message Service { + string name = 1; + } } diff --git a/builtin/k8s/releaser.go b/builtin/k8s/releaser.go index 2fcd4d1d775..089cb4a1ae2 100644 --- a/builtin/k8s/releaser.go +++ b/builtin/k8s/releaser.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/waypoint-plugin-sdk/component" "github.com/hashicorp/waypoint-plugin-sdk/docs" + "github.com/hashicorp/waypoint-plugin-sdk/framework/resource" sdk "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" "github.com/hashicorp/waypoint-plugin-sdk/terminal" ) @@ -50,38 +51,44 @@ func (r *Releaser) StatusFunc() interface{} { return r.Status } -// Release creates a Kubernetes service configured for the deployment -func (r *Releaser) Release( +func (r *Releaser) resourceManager(log hclog.Logger) *resource.Manager { + return resource.NewManager( + resource.WithLogger(log.Named("resource_manager")), + resource.WithValueProvider(r.getClientset), + resource.WithResource(resource.NewResource( + resource.WithName("service"), + resource.WithState(&Resource_Service{}), + resource.WithCreate(r.resourceServiceCreate), + resource.WithDestroy(r.resourceServiceDestroy), + )), + ) +} + +func (r *Releaser) resourceServiceCreate( ctx context.Context, log hclog.Logger, - src *component.Source, - ui terminal.UI, target *Deployment, -) (*Release, error) { - var result Release - result.ServiceName = src.App - sg := ui.StepGroup() + result *Release, + state *Resource_Service, + csinfo *clientsetInfo, + sg terminal.StepGroup, +) error { step := sg.Add("Initializing Kubernetes client...") defer func() { step.Abort() }() // Defer in func in case more steps are added to this func in the future - - // Get our clientset - clientset, ns, config, err := clientset(r.config.KubeconfigPath, r.config.Context) - if err != nil { - return nil, err - } - - // Override namespace if set + // Prepare our namespace and override if set. + ns := csinfo.Namespace if r.config.Namespace != "" { ns = r.config.Namespace } - step.Update("Kubernetes client connected to %s with namespace %s", config.Host, ns) + step.Update("Kubernetes client connected to %s with namespace %s", csinfo.Config.Host, ns) step.Done() step = sg.Add("Preparing service...") - serviceclient := clientset.CoreV1().Services(ns) + clientSet := csinfo.Clientset + serviceclient := clientSet.CoreV1().Services(ns) // Determine if we have a deployment that we manage already create := false @@ -92,9 +99,13 @@ func (r *Releaser) Release( err = nil } if err != nil { - return nil, err + return err } + // set the service name in state, at this point we've either created it or + // it already existed + state.Name = result.ServiceName + // Update the spec service.Spec.Selector = map[string]string{ "name": target.Name, @@ -102,8 +113,8 @@ func (r *Releaser) Release( } if (r.config.Port != 0 || r.config.NodePort != 0) && r.config.Ports != nil { - return nil, fmt.Errorf("Cannot define both 'ports' and 'port' or 'node_port'." + - " Use 'ports' for configuring multiple service ports.") + return fmt.Errorf("cannot define both 'ports' and 'port' or 'node_port'." + + " Use 'ports' for configuring multiple service ports") } else if r.config.Ports == nil && (r.config.Port != 0 || r.config.NodePort != 0) { r.config.Ports = make([]map[string]string, 1) r.config.Ports[0] = map[string]string{ @@ -182,7 +193,7 @@ func (r *Releaser) Release( service, err = serviceclient.Update(ctx, service, metav1.UpdateOptions{}) } if err != nil { - return nil, err + return err } step.Done() @@ -201,8 +212,9 @@ func (r *Releaser) Release( return service.Spec.ClusterIP != "", nil } }) + if err != nil { - return nil, err + return err } step.Update("Service is ready!") @@ -219,10 +231,10 @@ func (r *Releaser) Release( result.Url = fmt.Sprintf("%s:%d", result.Url, service.Spec.Ports[0].Port) } } else if service.Spec.Ports[0].NodePort > 0 { - nodeclient := clientset.CoreV1().Nodes() + nodeclient := clientSet.CoreV1().Nodes() nodes, err := nodeclient.List(ctx, metav1.ListOptions{}) if err != nil { - return nil, err + return err } nodeIP := nodes.Items[0].Status.Addresses[0].Address @@ -231,45 +243,31 @@ func (r *Releaser) Release( result.Url = fmt.Sprintf("http://%s:%d", service.Spec.ClusterIP, service.Spec.Ports[0].Port) } - return &result, nil + return nil } -// Destroy deletes the K8S deployment. -func (r *Releaser) Destroy( +func (r *Releaser) resourceServiceDestroy( ctx context.Context, - log hclog.Logger, - release *Release, - ui terminal.UI, + state *Resource_Service, + sg terminal.StepGroup, + csinfo *clientsetInfo, ) error { - // This is possible if an older version of the Kubernetes plugin was used - // prior to service name existing. This was only in pre-0.1 releases so - // we just return nil and pretend the destroy succeeded. We can probably - // remove this very quickly post-release. - if release.ServiceName == "" { - return nil - } - - sg := ui.StepGroup() step := sg.Add("Initializing Kubernetes client...") defer step.Abort() - // Get our client - clientset, ns, config, err := clientset(r.config.KubeconfigPath, r.config.Context) - if err != nil { - return err - } - - // Override namespace if set + // Prepare our namespace and override if set. + ns := csinfo.Namespace if r.config.Namespace != "" { ns = r.config.Namespace } - step.Update("Kubernetes client connected to %s with namespace %s", config.Host, ns) + clientSet := csinfo.Clientset + serviceclient := clientSet.CoreV1().Services(ns) + step.Update("Kubernetes client connected to %s with namespace %s", csinfo.Config.Host, ns) step.Done() - step = sg.Add("Deleting service...") - serviceclient := clientset.CoreV1().Services(ns) - if err := serviceclient.Delete(ctx, release.ServiceName, metav1.DeleteOptions{}); err != nil { + step = sg.Add("Deleting service...") + if err := serviceclient.Delete(ctx, state.Name, metav1.DeleteOptions{}); err != nil { return err } @@ -277,6 +275,81 @@ func (r *Releaser) Destroy( return nil } +// getClientset is a value provider for our resource manager and provides +// the connection information used by resources to interact with Kubernetes. +func (r *Releaser) getClientset() (*clientsetInfo, error) { + // Get our client + clientSet, ns, config, err := clientset(r.config.KubeconfigPath, r.config.Context) + if err != nil { + return nil, err + } + + return &clientsetInfo{ + Clientset: clientSet, + Namespace: ns, + Config: config, + }, nil +} + +// Release creates a Kubernetes service configured for the deployment +func (r *Releaser) Release( + ctx context.Context, + log hclog.Logger, + src *component.Source, + ui terminal.UI, + target *Deployment, +) (*Release, error) { + var result Release + result.ServiceName = src.App + + sg := ui.StepGroup() + defer sg.Wait() + + // Create our resource manager and create + rm := r.resourceManager(log) + if err := rm.CreateAll( + ctx, log, sg, ui, + target, &result, + ); err != nil { + return nil, err + } + + // Store our resource state + result.ResourceState = rm.State() + + return &result, nil +} + +// Destroy deletes the K8S deployment. +func (r *Releaser) Destroy( + ctx context.Context, + log hclog.Logger, + release *Release, + ui terminal.UI, +) error { + + sg := ui.StepGroup() + defer sg.Wait() + + rm := r.resourceManager(log) + + // If we don't have resource state, this state is from an older version + // and we need to manually recreate it. + if release.ResourceState == nil { + rm.Resource("service").SetState(&Resource_Service{ + Name: release.ServiceName, + }) + } else { + // Load our set state + if err := rm.LoadState(release.ResourceState); err != nil { + return err + } + } + + // Destroy + return rm.DestroyAll(ctx, log, sg, ui) +} + func (r *Releaser) Status( ctx context.Context, log hclog.Logger, @@ -461,12 +534,10 @@ func (r *Releaser) Documentation() (*docs.Documentation, error) { return doc, nil } -var ( - mixedHealthReleaseWarn = strings.TrimSpace(` +var mixedHealthReleaseWarn = strings.TrimSpace(` Waypoint detected that the current release is not ready, however your application might be available or still starting up. `) -) var ( _ component.ReleaseManager = (*Releaser)(nil)