fix: playbook delete is error (#2654)

Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
liujian 2025-07-10 14:04:24 +08:00 committed by GitHub
parent b68c73de2d
commit 39657b3dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 164 additions and 78 deletions

View File

@ -1,23 +0,0 @@
package v1
import (
"github.com/cockroachdb/errors"
"k8s.io/apimachinery/pkg/runtime"
)
const PlaybookFieldPlaybook = "spec.playbook"
// AddConversionFuncs adds the conversion functions to the given scheme.
// NOTE: ownerReferences:playbook is valid in proxy client.
func AddConversionFuncs(scheme *runtime.Scheme) error {
return scheme.AddFieldLabelConversionFunc(
SchemeGroupVersion.WithKind("Playbook"),
func(label, value string) (string, string, error) {
if label == PlaybookFieldPlaybook {
return label, value, nil
}
return "", "", errors.Errorf("field label %q not supported for Playbook", label)
},
)
}

View File

@ -0,0 +1,32 @@
package v1alpha1
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
)
// supportedFields defines the set of field labels that are allowed for field selector queries
// on the Task resource. Only these fields can be used in field selectors for filtering.
var supportedFields = sets.NewString(
"metadata.name",
"metadata.namespace",
"playbook.name",
"playbook.uid",
)
// RegisterFieldLabelConversion registers a field label conversion function for the Task resource.
// This function ensures that only supported field labels can be used in field selectors.
// If an unsupported field label is used, an error is returned.
func RegisterFieldLabelConversion(scheme *runtime.Scheme) error {
return scheme.AddFieldLabelConversionFunc(
SchemeGroupVersion.WithKind("Task"),
func(label, value string) (string, string, error) {
if !supportedFields.Has(label) {
return "", "", fmt.Errorf("field label %q is not supported", label)
}
return label, value, nil
},
)
}

View File

@ -55,5 +55,7 @@ func newScheme() *runtime.Scheme {
utilruntime.Must(clusterv1beta1.AddToScheme(s))
utilruntime.Must(kubeadmcpv1beta1.AddToScheme(s))
utilruntime.Must(kkcorev1alpha1.RegisterFieldLabelConversion(s))
return s
}

View File

@ -64,7 +64,9 @@ type playbookExecutor struct {
// Exec playbook. covert playbook to block and executor it.
func (e playbookExecutor) Exec(ctx context.Context) (retErr error) {
defer e.syncStatus(ctx, retErr)
defer func() {
e.syncStatus(ctx, retErr)
}()
fmt.Fprint(e.logOutput, `
_ __ _ _ __

View File

@ -141,7 +141,7 @@ func (w *fileWatcher) watch() {
// watchFile for resource.
func (w *fileWatcher) watchFile(event fsnotify.Event) error {
if !strings.HasSuffix(event.Name, yamlSuffix) {
if !strings.HasSuffix(event.Name, yamlSuffix) && !strings.HasSuffix(event.Name, yamlSuffix+deleteTagSuffix) {
return nil
}
data, err := os.ReadFile(event.Name)
@ -164,11 +164,6 @@ func (w *fileWatcher) watchFile(event fsnotify.Event) error {
switch event.Op {
case fsnotify.Create:
w.watchEvents <- watch.Event{
Type: watch.Added,
Object: obj,
}
case fsnotify.Write:
if strings.HasSuffix(filepath.Base(event.Name), deleteTagSuffix) {
// delete event
w.watchEvents <- watch.Event{
@ -179,12 +174,17 @@ func (w *fileWatcher) watchFile(event fsnotify.Event) error {
klog.ErrorS(err, "failed to remove file", "event", event)
}
} else {
// update event
w.watchEvents <- watch.Event{
Type: watch.Modified,
Type: watch.Added,
Object: obj,
}
}
case fsnotify.Write:
// update event
w.watchEvents <- watch.Event{
Type: watch.Modified,
Object: obj,
}
}
return nil

View File

@ -16,6 +16,11 @@ limitations under the License.
package api
const (
// SchemaLabelSubfix is the label key used to indicate which schema a playbook belongs to.
SchemaLabelSubfix = "kubekey.kubesphere.io/schema"
)
const (
// ResultSucceed indicates a successful operation result.
ResultSucceed = "success"
@ -69,15 +74,15 @@ type InventoryHostGroups struct {
// It includes fields such as name, type, title, description, version, namespace, logo, priority, and associated playbooks.
// The Playbook field is a slice of SchemaTablePlaybook, each representing a playbook reference.
type SchemaTable struct {
Name string `json:"name"` // Name of schema, defined by filename
SchemaType string `json:"schemaType"` // Type of the schema (e.g., CRD, built-in)
Title string `json:"title"` // Title of the schema
Description string `json:"description"` // Description of the schema
Version string `json:"version"` // Version of the schema
Namespace string `json:"namespace"` // Namespace of the schema
Logo string `json:"logo"` // Logo URL or identifier
Priority int `json:"priority"` // Priority for display or ordering
Playbook []SchemaTablePlaybook `json:"playbook"` // List of reference playbooks
Name string `json:"name"` // Name of schema, defined by filename
SchemaType string `json:"schemaType"` // Type of the schema (e.g., CRD, built-in)
Title string `json:"title"` // Title of the schema
Description string `json:"description"` // Description of the schema
Version string `json:"version"` // Version of the schema
Namespace string `json:"namespace"` // Namespace of the schema
Logo string `json:"logo"` // Logo URL or identifier
Priority int `json:"priority"` // Priority for display or ordering
Playbook SchemaTablePlaybook `json:"playbook"` // List of reference playbooks
}
// SchemaTablePlaybook represents a reference to a playbook associated with a schema.

View File

@ -15,6 +15,7 @@ import (
"sync"
"time"
"github.com/cockroachdb/errors"
restfulspec "github.com/emicklei/go-restful-openapi/v2"
"github.com/emicklei/go-restful/v3"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
@ -38,15 +39,9 @@ import (
// NewCoreService creates and configures a new RESTful web service for managing inventories and playbooks.
// It sets up routes for CRUD operations on inventories and playbooks, including pagination, sorting, and filtering.
func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Config) *restful.WebService {
ws := new(restful.WebService)
// the GroupVersion might be empty, we need to remove the final /
ws.Path(strings.TrimRight(_const.APIPath+kkcorev1.SchemeGroupVersion.String(), "/")).
Produces(restful.MIME_JSON).Consumes(
string(types.JSONPatchType),
string(types.MergePatchType),
string(types.StrategicMergePatchType),
string(types.ApplyPatchType),
restful.MIME_JSON)
ws := new(restful.WebService).
// the GroupVersion might be empty, we need to remove the final /
Path(strings.TrimRight(_const.APIPath+kkcorev1.SchemeGroupVersion.String(), "/"))
h := newCoreHandler(workdir, client, config)
@ -54,20 +49,23 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.POST("/inventories").To(h.createInventory).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("create a inventory.").
Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).
Reads(kkcorev1.Inventory{}).
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
ws.Route(ws.PATCH("/namespaces/{namespace}/inventories/{inventory}").To(h.patchInventory).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("patch a inventory.").
Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.ApplyPatchType)).Produces(restful.MIME_JSON).
Reads(kkcorev1.Inventory{}).
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
Param(ws.PathParameter("inventory", "the name of the inventory")).
Reads(kkcorev1.Inventory{}).
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
ws.Route(ws.GET("/inventories").To(h.listInventories).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("list all inventories.").
Produces(restful.MIME_JSON).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("false")).
@ -77,6 +75,7 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.GET("/namespaces/{namespace}/inventories").To(h.listInventories).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("list all inventories in a namespace.").
Produces(restful.MIME_JSON).
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
@ -87,6 +86,7 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.GET("/namespaces/{namespace}/inventories/{inventory}").To(h.inventoryInfo).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("get a inventory in a namespace.").
Produces(restful.MIME_JSON).
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
Param(ws.PathParameter("inventory", "the name of the inventory")).
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
@ -94,6 +94,7 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.GET("/namespaces/{namespace}/inventories/{inventory}/hosts").To(h.listInventoryHosts).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("list all hosts in a inventory.").
Produces(restful.MIME_JSON).
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
Param(ws.PathParameter("inventory", "the name of the inventory")).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
@ -106,13 +107,14 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.POST("/playbooks").To(h.createPlaybook).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("create a playbook.").
Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).
Reads(kkcorev1.Playbook{}).
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Playbook{}))
ws.Route(ws.GET("/playbooks").To(h.listPlaybooks).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("list all playbooks.").
Reads(kkcorev1.Playbook{}).
Produces(restful.MIME_JSON).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("false")).
@ -122,6 +124,7 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.GET("/namespaces/{namespace}/playbooks").To(h.listPlaybooks).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("list all playbooks in a namespace.").
Produces(restful.MIME_JSON).
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
@ -132,6 +135,7 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.GET("/namespaces/{namespace}/playbooks/{playbook}").To(h.playbookInfo).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("get or watch a playbook in a namespace.").
Produces(restful.MIME_JSON).
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
Param(ws.PathParameter("playbook", "the name of the playbook")).
Param(ws.QueryParameter("watch", "set to true to watch this playbook")).
@ -140,13 +144,15 @@ func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Confi
ws.Route(ws.GET("/namespaces/{namespace}/playbooks/{playbook}/log").To(h.logPlaybook).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("get a playbook execute log.").
Produces("text/plain").
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
Param(ws.PathParameter("playbook", "the name of the playbook")).
Returns(http.StatusOK, _const.StatusOK, "text/plain"))
Returns(http.StatusOK, _const.StatusOK, ""))
ws.Route(ws.DELETE("/namespaces/{namespace}/playbooks/{playbook}").To(h.deletePlaybook).
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
Doc("delete a playbook.").
Produces(restful.MIME_JSON).
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
Param(ws.PathParameter("playbook", "the name of the playbook")).
Returns(http.StatusOK, _const.StatusOK, api.Result{}))
@ -469,37 +475,83 @@ func (h *coreHandler) listInventoryHosts(request *restful.Request, response *res
// It reads the playbook from the request, sets the workdir, creates the resource, and starts execution in a goroutine.
func (h *coreHandler) createPlaybook(request *restful.Request, response *restful.Response) {
playbook := &kkcorev1.Playbook{}
// Read the playbook entity from the request body
if err := request.ReadEntity(playbook); err != nil {
api.HandleBadRequest(response, request, err)
return
}
// Set workdir to playbook spec config.
// Check for schema label: only one allowed, must not be empty, and must be unique among playbooks
hasSchemaLabel := false
for labelKey, labelValue := range playbook.Labels {
// Only consider labels with the schema label suffix
if !strings.HasSuffix(labelKey, api.SchemaLabelSubfix) {
continue
}
// If a schema label was already found, this is a conflict
if hasSchemaLabel {
api.HandleConflict(response, request, errors.New("a playbook can only have one schema label. Please ensure only one schema label is set"))
return
}
// The schema label value must not be empty
if labelValue == "" {
api.HandleConflict(response, request, errors.New("the schema label value must not be empty. Please provide a valid schema label value"))
return
}
hasSchemaLabel = true
// Check if there is already a playbook with the same schema label
playbookList := &kkcorev1.PlaybookList{}
if err := h.client.List(request.Request.Context(), playbookList, ctrlclient.MatchingLabels{
labelKey: labelValue,
}); err != nil {
api.HandleBadRequest(response, request, err)
return
}
// If any playbook with the same schema label exists, this is a conflict
if len(playbookList.Items) > 0 {
api.HandleConflict(response, request, errors.New("a playbook with the same schema label already exists. Please use a different schema label or remove the existing playbook"))
return
}
}
// Set the workdir in the playbook's spec config
if err := unstructured.SetNestedField(playbook.Spec.Config.Value(), h.workdir, _const.Workdir); err != nil {
api.HandleBadRequest(response, request, err)
return
}
// Create the playbook resource in the cluster
if err := h.client.Create(request.Request.Context(), playbook); err != nil {
api.HandleBadRequest(response, request, err)
return
}
// Start playbook execution in a separate goroutine
go func() {
// Create playbook log file and execute the playbook, writing output to the log.
filename := filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.Group, kkcorev1.SchemeGroupVersion.Version, "playbooks", playbook.Namespace, playbook.Name, playbook.Name+".log")
// Check if the directory for the log file exists, and create it if it does not.
// Build the log file path for the playbook execution
filename := filepath.Join(
_const.GetWorkdirFromConfig(playbook.Spec.Config),
_const.RuntimeDir,
kkcorev1.SchemeGroupVersion.Group,
kkcorev1.SchemeGroupVersion.Version,
"playbooks",
playbook.Namespace,
playbook.Name,
playbook.Name+".log",
)
// Ensure the directory for the log file exists
if _, err := os.Stat(filepath.Dir(filename)); err != nil {
if !os.IsNotExist(err) {
api.HandleBadRequest(response, request, err)
return
}
// If directory does not exist, create it.
// If directory does not exist, create it
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
api.HandleBadRequest(response, request, err)
return
}
}
// Open the log file for writing.
// Open the log file for writing
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
klog.ErrorS(err, "failed to open file", "file", filename)
@ -507,15 +559,15 @@ func (h *coreHandler) createPlaybook(request *restful.Request, response *restful
}
defer file.Close()
// Create a cancellable context for the playbook execution.
// Create a cancellable context for playbook execution
ctx, cancel := context.WithCancel(context.Background())
// Add the playbook and its cancel function to the playbookManager.
// Register the playbook and its cancel function in the playbookManager
h.playbookManager.addPlaybook(playbook, cancel)
// Execute the playbook and write output to the log file.
// Execute the playbook and write output to the log file
if err := executor.NewPlaybookExecutor(ctx, h.client, playbook, file).Exec(ctx); err != nil {
klog.ErrorS(err, "failed to exec playbook", "playbook", playbook.Name)
}
// Remove the playbook from the playbookManager after execution.
// Remove the playbook from the playbookManager after execution
h.playbookManager.deletePlaybook(playbook)
}()
@ -697,10 +749,11 @@ func (h *coreHandler) deletePlaybook(request *restful.Request, response *restful
}
// delete relative filepath: variable and log
_ = os.Remove(filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.Group, kkcorev1.SchemeGroupVersion.Version, "playbooks", playbook.Namespace, playbook.Name, playbook.Name+".log"))
_ = os.RemoveAll(filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.Group, kkcorev1.SchemeGroupVersion.Version, "playbooks", playbook.Namespace, playbook.Name, playbook.Name))
_ = os.RemoveAll(filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.Group, kkcorev1.SchemeGroupVersion.Version, "playbooks", playbook.Namespace, playbook.Name))
// Delete all tasks owned by this playbook.
if err := h.client.DeleteAllOf(request.Request.Context(), &kkcorev1alpha1.Task{}, ctrlclient.MatchingFields{
"playbook.name": playbook.Name, "playbook.namespace": playbook.Namespace,
if err := h.client.DeleteAllOf(request.Request.Context(), &kkcorev1alpha1.Task{}, ctrlclient.InNamespace(playbook.Namespace), ctrlclient.MatchingFields{
"playbook.name": playbook.Name,
"playbook.uid": string(playbook.UID),
}); err != nil {
api.HandleError(response, request, err)
return

View File

@ -2,7 +2,7 @@ package web
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
@ -15,6 +15,7 @@ import (
"sync"
"time"
"github.com/cockroachdb/errors"
restfulspec "github.com/emicklei/go-restful-openapi/v2"
"github.com/emicklei/go-restful/v3"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
@ -44,6 +45,7 @@ func NewSchemaService(rootPath string, workdir string, client ctrlclient.Client)
Doc("list available ip from ip cidr").
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}).
Param(ws.QueryParameter("cidr", "the cidr for ip").Required(true)).
Param(ws.QueryParameter("sshPort", "the ssh port for ip").Required(false)).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("false")).
@ -57,7 +59,7 @@ func NewSchemaService(rootPath string, workdir string, client ctrlclient.Client)
Doc("list all schema as table").
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}).
Param(ws.QueryParameter("schemaType", "the type of schema json").Required(false)).
Param(ws.QueryParameter("playbookLabel", "the reference playbook of schema. eg: install.kubekey.kubesphere.io/schema,check.kubekey.kubesphere.io/schema"+
Param(ws.QueryParameter("playbookLabel", "the reference playbook of schema. eg: \"install.kubekey.kubesphere.io/schema\", \"check.kubekey.kubesphere.io/schema\" \\n"+
"if empty will not return any reference playbook").Required(false)).
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
@ -83,10 +85,14 @@ type schemaHandler struct {
func (h schemaHandler) listIP(request *restful.Request, response *restful.Response) {
queryParam := query.ParseQueryParameter(request)
cidr, ok := queryParam.Filters["cidr"]
if !ok || len(cidr) == 0 {
if !ok || string(cidr) == "" {
api.HandleBadRequest(response, request, errors.New("cidr parameter is required"))
return
}
sshPort, ok := queryParam.Filters["sshPort"]
if !ok || string(sshPort) == "" {
sshPort = "22"
}
ips := _const.ParseIP(string(cidr))
ipTable := make([]api.IPTable, 0, len(ips))
maxConcurrency := 20
@ -115,7 +121,7 @@ func (h schemaHandler) listIP(request *restful.Request, response *restful.Respon
if !ifIPOnline(ip) {
continue
}
reachable, authorized := ifIPSSHAuthorized(ip)
reachable, authorized := ifIPSSHAuthorized(ip, string(sshPort))
mu.Lock()
ipTable = append(ipTable, api.IPTable{
@ -220,13 +226,22 @@ func (h schemaHandler) allSchema(request *restful.Request, response *restful.Res
api.HandleBadRequest(response, request, err)
return
}
schema.Playbook = make([]api.SchemaTablePlaybook, len(playbookList.Items))
for i, playbook := range playbookList.Items {
schema.Playbook[i] = api.SchemaTablePlaybook{
Name: playbook.Name,
Namespace: playbook.Namespace,
Phase: string(playbook.Status.Phase),
switch len(playbookList.Items) {
case 0: // skip
case 1:
item := &playbookList.Items[0]
schema.Playbook = api.SchemaTablePlaybook{
Name: item.Name,
Namespace: item.Namespace,
Phase: string(item.Status.Phase),
}
default:
playbookNames := make([]string, 0, len(playbookList.Items))
for _, playbook := range playbookList.Items {
playbookNames = append(playbookNames, playbook.Name)
}
api.HandleBadRequest(response, request, errors.Errorf("schema %q has multiple playbooks: %q", entry.Name(), playbookNames))
return
}
}
schemaTable = append(schemaTable, schema)
@ -439,9 +454,9 @@ func isValidICMPReply(n int, reply []byte, src net.Addr, expectedIP net.IP, prot
// ifIPSSHAuthorized checks if SSH authorization to the given IP is possible using the local private key.
// It returns two booleans: the first indicates if the SSH port (22) is reachable, and the second indicates if SSH authorization using the local private key is successful.
// The function attempts to find the user's private key, read and parse it, and then connect via SSH.
func ifIPSSHAuthorized(ipStr string) (bool, bool) {
func ifIPSSHAuthorized(ipStr, sshPort string) (bool, bool) {
// First check if port 22 is reachable on the target IP address.
conn, err := net.DialTimeout("tcp", ipStr+":22", time.Second)
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", ipStr, sshPort), time.Second)
if err != nil {
klog.V(6).Infof("port 22 not reachable on ip %q, error %v", ipStr, err)
return false, false