Repository: rancher/shepherd
Pull request #394: Add ForConfig functions for functions in Rancher and Harvester Clients
Author: login: git-ival
Labels: name: automation-fix
description: To indicate this is related to fixing code related to automation
name: automation-enhancement
description: To indicate this is related to enhancements in code related to automation
Created: 2025-05-13T17:33:36Z | Merged: 2025-05-14T18:24:50Z

## Issue: N/A

## Problem
1. Using the `NewClientForConfig()` Rancher client function with `ReLogin()` or other such functions could result in errors, due to the fact that they default to calling the `NewClient()` function which uses Shepherd's config file loading mechanism. Due to the nature of the `NewClientForConfig()` flow expecting a Config struct to be passed directly (bypassing Shepherd's config loading logic entirely), the referenced functions would fail to load the config file when no such config file exists/is in use.

2. The newly added Harvester client had no mechanism to bypass Shepherd's config file loading mechanism. 

3. `IsPodReady()` in `pod_status.go` had both a `nil` check and a zero-length check for the `ContainerStatuses` slice, but this is unnecessary since a `nil` slice has length == 0.
 
## Solution
1. As a simple solution, I have added `ForConfig` function variants to all functions I have identified to use the `NewClient()` flow.

2. I have added a `NewClientForConfig()` to the Harvester client code, which the `NewClient()` function calls.
   - I have also removed usage of the `environmentflags` package, as the Harvester Client has no reference to `environmentflag.EnvironmentFlags` at all, like the Rancher client does.

3. Removed the `nil` check entirely, leaving only the zero-length check.
 
## Testing
1. This will not cause any regressions as it does not modify existing functions.

2. This should also not cause any regressions as it simply moves the former `NewClient()` flow into `NewClientForConfig()` and has the updated `NewClient()` call `NewClientForConfig()` with the Config that is loaded from file.

3. Utilized the updated `IsPodReady()` locally on many occasions.

Edit: Tested the Rancher and Harvester client package changes locally with @slickwarren's help!

### Regressions Considerations
1. 0%

2. 0% 

3. 0%


Diff:
diff --git a/clients/harvester/client.go b/clients/harvester/client.go
index 6d9715f9..cce8d40e 100644
--- a/clients/harvester/client.go
+++ b/clients/harvester/client.go
@@ -9,7 +9,6 @@ import (
 
 	"github.com/rancher/shepherd/pkg/clientbase"
 	"github.com/rancher/shepherd/pkg/config"
-	"github.com/rancher/shepherd/pkg/environmentflag"
 	"github.com/rancher/shepherd/pkg/session"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/rest"
@@ -36,32 +35,44 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
+	c, err := NewClientForConfig(bearerToken, harvesterConfig, session)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, err
+}
 
+// NewClientForConfig is the constructor for initializing a rancher Client for the given config and session.
+func NewClientForConfig(bearerToken string, harvesterConfig *Config, session *session.Session) (*Client, error) {
 	if bearerToken == "" {
 		bearerToken = harvesterConfig.AdminToken
 	}
 
+	if harvesterConfig == nil {
+		harvesterConfig = new(Config)
+	}
+
 	c := &Client{
 		HarvesterConfig: harvesterConfig,
 	}
 
-	session.CleanupEnabled = *harvesterConfig.Cleanup
+	if session != nil {
+		session.CleanupEnabled = *c.HarvesterConfig.Cleanup
+	}
 
 	var err error
-	restConfig := newRestConfig(bearerToken, harvesterConfig)
-	c.restConfig = restConfig
+	c.restConfig = newRestConfig(bearerToken, c.HarvesterConfig)
 	c.Session = session
 
-	c.Steve, err = v1.NewClient(clientOptsV1(restConfig, c.HarvesterConfig))
+	c.Steve, err = v1.NewClient(clientOptsV1(c.restConfig, c.HarvesterConfig))
 	if err != nil {
 		return nil, err
 	}
 
 	c.Steve.Ops.Session = session
 
-	catalogClient, err := catalog.NewForConfig(restConfig, session)
+	catalogClient, err := catalog.NewForConfig(c.restConfig, session)
 	if err != nil {
 		return nil, err
 	}
@@ -71,7 +82,7 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	return c, nil
 }
 
-// newRestConfig is a constructor that sets ups rest.Config the configuration used by the Provisioning client.
+// newRestConfig is a constructor that sets up a rest.Config the configuration used by the Provisioning client.
 func newRestConfig(bearerToken string, harvesterConfig *Config) *rest.Config {
 	return &rest.Config{
 		Host:        harvesterConfig.Host,
@@ -82,7 +93,7 @@ func newRestConfig(bearerToken string, harvesterConfig *Config) *rest.Config {
 	}
 }
 
-// clientOptsV1 is a constructor that sets ups clientbase.ClientOpts the configuration used by the v1 harvester clients.
+// clientOptsV1 is a constructor that sets up a clientbase.ClientOpts the configuration used by the v1 harvester clients.
 func clientOptsV1(restConfig *rest.Config, harvesterConfig *Config) *clientbase.ClientOpts {
 	return &clientbase.ClientOpts{
 		URL:      fmt.Sprintf("https://%s/v1", harvesterConfig.Host),
diff --git a/clients/rancher/client.go b/clients/rancher/client.go
index 2e757774..0000ac50 100644
--- a/clients/rancher/client.go
+++ b/clients/rancher/client.go
@@ -228,18 +228,41 @@ func (c *Client) AsUser(user *management.User) (*Client, error) {
 	return NewClient(returnedToken.Token, c.Session)
 }
 
+// AsUserForConfig accepts a Config and a user object, and then creates a token for said `user`. Then it instantiates and returns a Client using the token created.
+// This function uses the login action, and user must have a correct username and password combination.
+func (c *Client) AsUserForConfig(rancherConfig *Config, user *management.User) (*Client, error) {
+	returnedToken, err := c.login(user)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewClientForConfig(returnedToken.Token, rancherConfig, c.Session)
+}
+
 // ReLogin reinstantiates a Client to update its API schema. This function would be used for a non admin user that needs to be
 // "reloaded" inorder to have updated permissions for certain resources.
 func (c *Client) ReLogin() (*Client, error) {
 	return NewClient(c.restConfig.BearerToken, c.Session)
 }
 
+// ReLoginForconfig reinstantiates a Client to update its API schema with the same Config. This function would be used for a non admin user that needs to be
+// "reloaded" inorder to have updated permissions for certain resources.
+func (c *Client) ReLoginForConfig(rancherConfig *Config) (*Client, error) {
+	return NewClientForConfig(c.restConfig.BearerToken, rancherConfig, c.Session)
+}
+
 // WithSession accepts a session.Session and instantiates a new Client to reference this new session.Session. The main purpose is to use it
 // when created "sub sessions" when tracking resources created at a test case scope.
 func (c *Client) WithSession(session *session.Session) (*Client, error) {
 	return NewClient(c.restConfig.BearerToken, session)
 }
 
+// WithSessionForConfig accepts a Config and a session.Session and instantiates a new Client to reference this new session.Session. The main purpose is to use it
+// when created "sub sessions" when tracking resources created at a test case scope.
+func (c *Client) WithSessionForConfig(rancherConfig *Config, session *session.Session) (*Client, error) {
+	return NewClientForConfig(c.restConfig.BearerToken, rancherConfig, session)
+}
+
 // GetClusterCatalogClient is a function that takes a clusterID and instantiates a catalog client to directly communicate with that specific cluster.
 func (c *Client) GetClusterCatalogClient(clusterID string) (*catalog.Client, error) {
 	restConfig := *c.restConfig
diff --git a/extensions/workloads/pods/pod_status.go b/extensions/workloads/pods/pod_status.go
index af48964d..7a363068 100644
--- a/extensions/workloads/pods/pod_status.go
+++ b/extensions/workloads/pods/pod_status.go
@@ -65,7 +65,7 @@ func IsPodReady(pod *v1.SteveAPIObject) (bool, error) {
 		return false, err
 	}
 
-	if podStatus.ContainerStatuses == nil || len(podStatus.ContainerStatuses) == 0 {
+	if len(podStatus.ContainerStatuses) == 0 {
 		return false, nil
 	}
 


Reviews:
user: login: slickwarren
body: my only nit is with the naming; would you consider replacing the part of the name `forConfig` in each function with something more descriptive? i.e. `NewClientFromConfig` ? 

also I know you say there's no chance for regression but a smoke test would be nice, I can share the harvester job with you if you'd like. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:09:31Z
user: login: rancher-max
body: On the `*ForConfig` functions defined in clients/rancher/client.go, is it not feasible to just call NewClientForConfig directly in all cases instead of the intermediary functions that don't do anything else? I'd argue that maybe `AsUserForConfig` has a use since it has some additional logic there, but even then it feels like the NewClientForConfig function should be sufficient
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T19:08:40Z
user: login: git-ival
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T20:12:05Z
user: login: git-ival
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T20:14:49Z
user: login: git-ival
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T20:30:19Z
user: login: slickwarren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T21:30:31Z
user: login: slickwarren
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T23:16:23Z
user: login: igomez06
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T14:41:42Z

Review comments:
diff_hunk: @@ -36,21 +35,34 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
+	c, err := NewClientForConfig(bearerToken, harvesterConfig, session)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, err
+}
 
+// NewClientForConfig is the constructor for initializing a rancher Client for the given config and session.
+func NewClientForConfig(bearerToken string, harvesterConfig *Config, session *session.Session) (*Client, error) {
 	if bearerToken == "" {
 		bearerToken = harvesterConfig.AdminToken
 	}
 
+	if harvesterConfig == nil {
+		harvesterConfig = new(Config)
+	}
+
 	c := &Client{
 		HarvesterConfig: harvesterConfig,
 	}
 
-	session.CleanupEnabled = *harvesterConfig.Cleanup
+	if session != nil {
+		session.CleanupEnabled = *c.HarvesterConfig.Cleanup
+	}
 
 	var err error
-	restConfig := newRestConfig(bearerToken, harvesterConfig)
+	restConfig := newRestConfig(bearerToken, c.HarvesterConfig)
 	c.restConfig = restConfig
path: clients/harvester/client.go
user: login: rancher-max
body: Can this just directly be assigned to c.restConfig?
created_at: 2025-05-13T18:51:57Z
author_association: MEMBER
diff_hunk: @@ -36,21 +35,34 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
path: clients/harvester/client.go
user: login: rancher-max
body: Are we losing this functional as part of these changes?
created_at: 2025-05-13T19:00:15Z
author_association: MEMBER
diff_hunk: @@ -36,21 +35,34 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
path: clients/harvester/client.go
user: login: git-ival
body: `environmentFlags` is never used by the Harvester Client or Config so not sure why it was here tbh.

@slickwarren Would know more
created_at: 2025-05-13T20:12:05Z
author_association: MEMBER
diff_hunk: @@ -36,21 +35,34 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
+	c, err := NewClientForConfig(bearerToken, harvesterConfig, session)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, err
+}
 
+// NewClientForConfig is the constructor for initializing a rancher Client for the given config and session.
+func NewClientForConfig(bearerToken string, harvesterConfig *Config, session *session.Session) (*Client, error) {
 	if bearerToken == "" {
 		bearerToken = harvesterConfig.AdminToken
 	}
 
+	if harvesterConfig == nil {
+		harvesterConfig = new(Config)
+	}
+
 	c := &Client{
 		HarvesterConfig: harvesterConfig,
 	}
 
-	session.CleanupEnabled = *harvesterConfig.Cleanup
+	if session != nil {
+		session.CleanupEnabled = *c.HarvesterConfig.Cleanup
+	}
 
 	var err error
-	restConfig := newRestConfig(bearerToken, harvesterConfig)
+	restConfig := newRestConfig(bearerToken, c.HarvesterConfig)
 	c.restConfig = restConfig
path: clients/harvester/client.go
user: login: git-ival
body: Sure, this is just how the code was originally written so I left it as-is
created_at: 2025-05-13T20:14:49Z
author_association: MEMBER
diff_hunk: @@ -36,21 +35,34 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
+	c, err := NewClientForConfig(bearerToken, harvesterConfig, session)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, err
+}
 
+// NewClientForConfig is the constructor for initializing a rancher Client for the given config and session.
+func NewClientForConfig(bearerToken string, harvesterConfig *Config, session *session.Session) (*Client, error) {
 	if bearerToken == "" {
 		bearerToken = harvesterConfig.AdminToken
 	}
 
+	if harvesterConfig == nil {
+		harvesterConfig = new(Config)
+	}
+
 	c := &Client{
 		HarvesterConfig: harvesterConfig,
 	}
 
-	session.CleanupEnabled = *harvesterConfig.Cleanup
+	if session != nil {
+		session.CleanupEnabled = *c.HarvesterConfig.Cleanup
+	}
 
 	var err error
-	restConfig := newRestConfig(bearerToken, harvesterConfig)
+	restConfig := newRestConfig(bearerToken, c.HarvesterConfig)
 	c.restConfig = restConfig
path: clients/harvester/client.go
user: login: git-ival
body: Done
created_at: 2025-05-13T20:30:19Z
author_association: MEMBER
diff_hunk: @@ -36,21 +35,34 @@ func NewClient(bearerToken string, session *session.Session) (*Client, error) {
 	harvesterConfig := new(Config)
 	config.LoadConfig(ConfigurationFileKey, harvesterConfig)
 
-	environmentFlags := environmentflag.NewEnvironmentFlags()
-	environmentflag.LoadEnvironmentFlags(environmentflag.ConfigurationFileKey, environmentFlags)
path: clients/harvester/client.go
user: login: slickwarren
body: yeah it can be rm'd afaik
created_at: 2025-05-13T21:30:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: git-ival
created_at: 2025-05-13T20:11:11Z
body: > my only nit is with the naming; would you consider replacing the part of the name `forConfig` in each function with something more descriptive? i.e. `NewClientFromConfig` ?
> 
> also I know you say there's no chance for regression but a smoke test would be nice, I can share the harvester job with you if you'd like.

@slickwarren The "ForConfig" suffix is how upstream k8s repos handle this type of thing, and its how we do so in the Rancher client so I adopted the same practice. If you still want me to change it I can do that no problem 👍 

Edit: See [client-go](https://github.com/search?q=repo%3Akubernetes%2Fclient-go%20ForConfig&type=code) for example

Also, I can run the harvester job, is there a way I can manually verify that the job actually completed successfully post-job run?
author_association: MEMBER
user: login: git-ival
created_at: 2025-05-13T20:21:15Z
body: > On the `*ForConfig` functions defined in clients/rancher/client.go, is it not feasible to just call NewClientForConfig directly in all cases instead of the intermediary functions that don't do anything else? I'd argue that maybe `AsUserForConfig` has a use since it has some additional logic there, but even then it feels like the NewClientForConfig function should be sufficient

@rancher-max Yes, realistically that should suffice, but it would not guarantee there are no possible regressions. I can't be sure that the behavior of re-loading the config file by calling `NewClient()` isn't expected/needed to update the underlying Config/Client in some way after an `UpdateConfig()` is called somewhere.

Context from `config.go`:
![image](https://github.com/user-attachments/assets/fd03d35d-db98-4caa-9a90-239a317dc6f2)


@slickwarren @igomez06 @caliskanugur Thoughts?
author_association: MEMBER
user: login: slickwarren
created_at: 2025-05-13T21:39:40Z
body: I don't have a strong preference for new functions vs. just using NewClientForConfig. If people are used to directly using these functions, then it would probably make sense to duplicate them for the ForConfig use cases so that they aren't confused if they switch to using it. But if there's hardly any use of these functions right now, then I would say to expect people to just use NewClientForConfig and not add the new functions (besides AsUserForConig as Max stated)


author_association: CONTRIBUTOR
user: login: git-ival
created_at: 2025-05-13T21:59:05Z
body: > I don't have a strong preference for new functions vs. just using NewClientForConfig. If people are used to directly using these functions, then it would probably make sense to duplicate them for the ForConfig use cases so that they aren't confused if they switch to using it. But if there's hardly any use of these functions right now, then I would say to expect people to just use NewClientForConfig and not add the new functions (besides AsUserForConig as Max stated)

@slickwarren Oh I definitely needed these functions, which is why I opened the PR 😅 
author_association: MEMBER