Create flag override-api-endpoint which allows for custom DNS setups

Issue #17262
This commit is contained in:
justinsb 2025-06-22 14:08:58 -04:00
parent adb6d81a9f
commit 138e14b1ad
12 changed files with 103 additions and 45 deletions

View File

@ -36,6 +36,7 @@ import (
"k8s.io/kops/pkg/cloudinstances"
"k8s.io/kops/pkg/commands/commandutils"
"k8s.io/kops/pkg/instancegroups"
"k8s.io/kops/pkg/kubeconfig"
"k8s.io/kops/pkg/validation"
"k8s.io/kops/upup/pkg/fi/cloudup"
"k8s.io/kubectl/pkg/util/i18n"
@ -71,6 +72,8 @@ type DeleteInstanceOptions struct {
InstanceID string
Surge bool
kubeconfig.CreateKubecfgOptions
}
func (o *DeleteInstanceOptions) initDefaults() {
@ -150,6 +153,8 @@ func NewCmdDeleteInstance(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().BoolVarP(&options.Yes, "yes", "y", options.Yes, "Specify --yes to immediately delete the instance")
options.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
return cmd
}
@ -167,12 +172,12 @@ func RunDeleteInstance(ctx context.Context, f *util.Factory, out io.Writer, opti
var k8sClient kubernetes.Interface
var restConfig *rest.Config
if !options.CloudOnly {
restConfig, err = f.RESTConfig(cluster)
restConfig, err = f.RESTConfig(ctx, cluster, options.CreateKubecfgOptions)
if err != nil {
return fmt.Errorf("getting rest config: %w", err)
}
httpClient, err := f.HTTPClient(cluster)
httpClient, err := f.HTTPClient(restConfig)
if err != nil {
return fmt.Errorf("getting http client: %w", err)
}

View File

@ -94,9 +94,12 @@ func NewCmdExportKubeconfig(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().Lookup("admin").NoOptDefVal = kubeconfig.DefaultKubecfgAdminLifetime.String()
cmd.Flags().StringVar(&options.User, "user", options.User, "Existing user in kubeconfig file to use")
cmd.RegisterFlagCompletionFunc("user", completeKubecfgUser)
cmd.Flags().BoolVar(&options.Internal, "internal", options.Internal, "Use the cluster's internal DNS name")
cmd.Flags().BoolVar(&options.UseKopsAuthenticationPlugin, "auth-plugin", options.UseKopsAuthenticationPlugin, "Use the kOps authentication plugin")
options.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
return cmd
}

View File

@ -26,6 +26,7 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/kops/pkg/cloudinstances"
"k8s.io/kops/pkg/commands/commandutils"
"k8s.io/kops/pkg/kubeconfig"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"sigs.k8s.io/yaml"
@ -64,7 +65,15 @@ type renderableCloudInstance struct {
State string `json:"state"`
}
type GetInstancesOptions struct {
*GetOptions
kubeconfig.CreateKubecfgOptions
}
func NewCmdGetInstances(f *util.Factory, out io.Writer, options *GetOptions) *cobra.Command {
opt := GetInstancesOptions{
GetOptions: options,
}
cmd := &cobra.Command{
Use: "instances [CLUSTER]",
Short: getInstancesShort,
@ -72,14 +81,16 @@ func NewCmdGetInstances(f *util.Factory, out io.Writer, options *GetOptions) *co
Args: rootCommand.clusterNameArgs(&options.ClusterName),
ValidArgsFunction: commandutils.CompleteClusterName(f, true, false),
RunE: func(cmd *cobra.Command, args []string) error {
return RunGetInstances(cmd.Context(), f, out, options)
return RunGetInstances(cmd.Context(), f, out, &opt)
},
}
opt.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
return cmd
}
func RunGetInstances(ctx context.Context, f *util.Factory, out io.Writer, options *GetOptions) error {
func RunGetInstances(ctx context.Context, f *util.Factory, out io.Writer, options *GetInstancesOptions) error {
clientset, err := f.KopsClient()
if err != nil {
return err
@ -99,12 +110,12 @@ func RunGetInstances(ctx context.Context, f *util.Factory, out io.Writer, option
return err
}
restConfig, err := f.RESTConfig(cluster)
restConfig, err := f.RESTConfig(ctx, cluster, options.CreateKubecfgOptions)
if err != nil {
return err
}
httpClient, err := f.HTTPClient(cluster)
httpClient, err := f.HTTPClient(restConfig)
if err != nil {
return err
}

View File

@ -219,6 +219,8 @@ func NewCmdRollingUpdateCluster(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().BoolVar(&options.FailOnDrainError, "fail-on-drain-error", true, "Fail if draining a node fails")
cmd.Flags().BoolVar(&options.FailOnValidate, "fail-on-validate-error", true, "Fail if the cluster fails to validate")
options.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
switch name {
case "ig", "instance-groups":
@ -233,7 +235,6 @@ func NewCmdRollingUpdateCluster(f *util.Factory, out io.Writer) *cobra.Command {
}
func RunRollingUpdateCluster(ctx context.Context, f *util.Factory, out io.Writer, options *RollingUpdateOptions) error {
f.CreateKubecfgOptions = options.CreateKubecfgOptions
clientset, err := f.KopsClient()
if err != nil {
return err
@ -247,12 +248,12 @@ func RunRollingUpdateCluster(ctx context.Context, f *util.Factory, out io.Writer
var nodes []v1.Node
var k8sClient kubernetes.Interface
if !options.CloudOnly {
restConfig, err := f.RESTConfig(cluster)
restConfig, err := f.RESTConfig(ctx, cluster, options.CreateKubecfgOptions)
if err != nil {
return fmt.Errorf("getting rest config: %w", err)
}
httpClient, err := f.HTTPClient(cluster)
httpClient, err := f.HTTPClient(restConfig)
if err != nil {
return fmt.Errorf("getting http client: %w", err)
}
@ -454,7 +455,7 @@ func RunRollingUpdateCluster(ctx context.Context, f *util.Factory, out io.Writer
var clusterValidator validation.ClusterValidator
if !options.CloudOnly {
restConfig, err := f.RESTConfig(cluster)
restConfig, err := f.RESTConfig(ctx, cluster, options.CreateKubecfgOptions)
if err != nil {
return fmt.Errorf("getting rest config: %w", err)
}

View File

@ -50,5 +50,7 @@ func NewCmdToolboxEnroll(f commandutils.Factory, out io.Writer) *cobra.Command {
cmd.Flags().StringVar(&options.SSHUser, "ssh-user", options.SSHUser, "user for ssh")
cmd.Flags().IntVar(&options.SSHPort, "ssh-port", options.SSHPort, "port for ssh")
options.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
return cmd
}

View File

@ -170,7 +170,10 @@ func NewCmdUpdateCluster(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().Lookup("admin").NoOptDefVal = kubeconfig.DefaultKubecfgAdminLifetime.String()
cmd.Flags().StringVar(&options.User, "user", options.User, "Existing user in kubeconfig file to use. Implies --create-kube-config")
cmd.RegisterFlagCompletionFunc("user", completeKubecfgUser)
cmd.Flags().BoolVar(&options.Internal, "internal", options.Internal, "Use the cluster's internal DNS name. Implies --create-kube-config")
options.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
cmd.Flags().BoolVar(&options.AllowKopsDowngrade, "allow-kops-downgrade", options.AllowKopsDowngrade, "Allow an older version of kOps to update the cluster than last used")
cmd.Flags().StringSliceVar(&options.InstanceGroups, "instance-group", options.InstanceGroups, "Instance groups to update (defaults to all if not specified)")
cmd.RegisterFlagCompletionFunc("instance-group", completeInstanceGroup(f, &options.InstanceGroups, &options.InstanceGroupRoles))

View File

@ -55,8 +55,6 @@ type Factory struct {
mutex sync.Mutex
// clusters holds REST connection configuration for connecting to clusters
clusters map[string]*clusterInfo
kubeconfig.CreateKubecfgOptions
}
// clusterInfo holds REST connection configuration for connecting to a cluster
@ -161,7 +159,7 @@ func (f *Factory) KopsStateStore() string {
return f.options.RegistryPath
}
func (f *Factory) getClusterInfo(cluster *kops.Cluster) *clusterInfo {
func (f *Factory) getClusterInfo(cluster *kops.Cluster, options kubeconfig.CreateKubecfgOptions) *clusterInfo {
f.mutex.Lock()
defer f.mutex.Unlock()
@ -170,22 +168,20 @@ func (f *Factory) getClusterInfo(cluster *kops.Cluster) *clusterInfo {
return clusterInfo
}
clusterInfo := &clusterInfo{
factory: f,
cluster: cluster,
factory: f,
cluster: cluster,
CreateKubecfgOptions: options,
}
f.clusters[key] = clusterInfo
return clusterInfo
}
func (f *Factory) RESTConfig(cluster *kops.Cluster) (*rest.Config, error) {
clusterInfo := f.getClusterInfo(cluster)
clusterInfo.CreateKubecfgOptions = f.CreateKubecfgOptions
return clusterInfo.RESTConfig()
func (f *Factory) RESTConfig(ctx context.Context, cluster *kops.Cluster, options kubeconfig.CreateKubecfgOptions) (*rest.Config, error) {
clusterInfo := f.getClusterInfo(cluster, options)
return clusterInfo.RESTConfig(ctx)
}
func (f *clusterInfo) RESTConfig() (*rest.Config, error) {
ctx := context.Background()
func (f *clusterInfo) RESTConfig(ctx context.Context) (*rest.Config, error) {
if f.cachedRESTConfig == nil {
restConfig, err := f.factory.buildRESTConfig(ctx, f.cluster, f.CreateKubecfgOptions)
if err != nil {
@ -201,17 +197,12 @@ func (f *clusterInfo) RESTConfig() (*rest.Config, error) {
return f.cachedRESTConfig, nil
}
func (f *Factory) HTTPClient(cluster *kops.Cluster) (*http.Client, error) {
clusterInfo := f.getClusterInfo(cluster)
return clusterInfo.HTTPClient()
func (f *Factory) HTTPClient(restConfig *rest.Config) (*http.Client, error) {
return rest.HTTPClientFor(restConfig)
}
func (f *clusterInfo) HTTPClient() (*http.Client, error) {
func (f *clusterInfo) HTTPClient(restConfig *rest.Config) (*http.Client, error) {
if f.cachedHTTPClient == nil {
restConfig, err := f.RESTConfig()
if err != nil {
return nil, err
}
httpClient, err := rest.HTTPClientFor(restConfig)
if err != nil {
return nil, fmt.Errorf("building http client: %w", err)
@ -222,19 +213,18 @@ func (f *clusterInfo) HTTPClient() (*http.Client, error) {
}
// DynamicClient returns a dynamic client
func (f *Factory) DynamicClient(cluster *kops.Cluster) (dynamic.Interface, error) {
clusterInfo := f.getClusterInfo(cluster)
return clusterInfo.DynamicClient()
func (f *Factory) DynamicClient(ctx context.Context, cluster *kops.Cluster, options kubeconfig.CreateKubecfgOptions) (dynamic.Interface, error) {
clusterInfo := f.getClusterInfo(cluster, options)
restConfig, err := clusterInfo.RESTConfig(ctx)
if err != nil {
return nil, err
}
return clusterInfo.DynamicClient(restConfig)
}
func (f *clusterInfo) DynamicClient() (dynamic.Interface, error) {
func (f *clusterInfo) DynamicClient(restConfig *rest.Config) (dynamic.Interface, error) {
if f.cachedDynamicClient == nil {
restConfig, err := f.RESTConfig()
if err != nil {
return nil, err
}
httpClient, err := f.HTTPClient()
httpClient, err := f.HTTPClient(restConfig)
if err != nil {
return nil, err
}

View File

@ -75,6 +75,8 @@ type ValidateClusterOptions struct {
// filterPodsForValidation is a function that returns true if the pod should be validated
filterPodsForValidation func(pod *v1.Pod) bool
ExportKubeconfigOptions
}
func (o *ValidateClusterOptions) InitDefaults() {
@ -117,6 +119,8 @@ func NewCmdValidateCluster(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().DurationVar(&options.interval, "interval", options.interval, "Time in duration to wait between validation attempts")
cmd.Flags().StringVar(&options.kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
options.CreateKubecfgOptions.AddCommonFlags(cmd.Flags())
return cmd
}
@ -155,12 +159,12 @@ func RunValidateCluster(ctx context.Context, f *util.Factory, out io.Writer, opt
return nil, fmt.Errorf("no InstanceGroup objects found")
}
restConfig, err := f.RESTConfig(cluster)
restConfig, err := f.RESTConfig(ctx, cluster, options.CreateKubecfgOptions)
if err != nil {
return nil, fmt.Errorf("getting rest config: %w", err)
}
httpClient, err := f.HTTPClient(cluster)
httpClient, err := f.HTTPClient(restConfig)
if err != nil {
return nil, fmt.Errorf("getting http client: %w", err)
}

View File

@ -17,15 +17,18 @@ limitations under the License.
package commandutils
import (
"context"
"k8s.io/client-go/rest"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/client/simple"
"k8s.io/kops/pkg/kubeconfig"
"k8s.io/kops/util/pkg/vfs"
)
type Factory interface {
KopsClient() (simple.Clientset, error)
VFSContext() *vfs.VFSContext
RESTConfig(cluster *kops.Cluster) (*rest.Config, error)
RESTConfig(ctx context.Context, cluster *kops.Cluster, options kubeconfig.CreateKubecfgOptions) (*rest.Config, error)
}

View File

@ -48,6 +48,7 @@ import (
"k8s.io/kops/pkg/client/simple"
"k8s.io/kops/pkg/commands/commandutils"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/pkg/kubeconfig"
"k8s.io/kops/pkg/model"
"k8s.io/kops/pkg/model/resources"
"k8s.io/kops/pkg/nodemodel"
@ -65,6 +66,8 @@ type ToolboxEnrollOptions struct {
SSHUser string
SSHPort int
kubeconfig.CreateKubecfgOptions
}
func (o *ToolboxEnrollOptions) InitDefaults() {
@ -108,7 +111,7 @@ func RunToolboxEnroll(ctx context.Context, f commandutils.Factory, out io.Writer
// Enroll the node over SSH.
if options.Host != "" {
restConfig, err := f.RESTConfig(fullCluster)
restConfig, err := f.RESTConfig(ctx, fullCluster, options.CreateKubecfgOptions)
if err != nil {
return err
}

View File

@ -24,6 +24,7 @@ import (
"sort"
"time"
"github.com/spf13/pflag"
"k8s.io/klog/v2"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/pki"
@ -42,6 +43,10 @@ type CreateKubecfgOptions struct {
// User is the user to use in the kubeconfig
User string
// OverrideAPIServer overrides the API endpoint to use in the kubeconfig
// This takes precedence over the Internal option (if set)
OverrideAPIServer string
// Internal is whether to use the internal API endpoint
Internal bool
@ -49,11 +54,19 @@ type CreateKubecfgOptions struct {
UseKopsAuthenticationPlugin bool
}
// AddCommonFlags adds the common flags to the flagset
// These are the flags that are used when building an internal connection to the cluster.
func (o *CreateKubecfgOptions) AddCommonFlags(flagset *pflag.FlagSet) {
flagset.StringVar(&o.OverrideAPIServer, "api-server", o.OverrideAPIServer, "Override the API server used when communicating with the cluster kube-apiserver")
}
func BuildKubecfg(ctx context.Context, cluster *kops.Cluster, keyStore fi.KeystoreReader, secretStore fi.SecretStore, cloud fi.Cloud, options CreateKubecfgOptions, kopsStateStore string) (*KubeconfigBuilder, error) {
clusterName := cluster.ObjectMeta.Name
var server string
if options.Internal {
if options.OverrideAPIServer != "" {
server = options.OverrideAPIServer
} else if options.Internal {
server = "https://" + cluster.APIInternalName()
} else {
if cluster.Spec.API.PublicName != "" {

View File

@ -405,6 +405,26 @@ func TestBuildKubecfg(t *testing.T) {
},
wantClientCert: false,
},
{
name: "Test Kube Config Data with APIEndpoint set",
args: args{
cluster: publicCluster,
status: fakeStatus,
CreateKubecfgOptions: CreateKubecfgOptions{
Admin: DefaultKubecfgAdminLifetime,
Internal: true,
OverrideAPIServer: "https://api.testcluster.example.com",
},
},
want: &KubeconfigBuilder{
Context: "testcluster",
Server: "https://api.testcluster.example.com",
TLSServerName: "api.internal.testcluster",
CACerts: []byte(nextCertificate + certData),
User: "testcluster",
},
wantClientCert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {