diff --git a/inttest/common/bootloosesuite.go b/inttest/common/bootloosesuite.go index 2a1a20f20089..c57f8e655c0a 100644 --- a/inttest/common/bootloosesuite.go +++ b/inttest/common/bootloosesuite.go @@ -817,15 +817,15 @@ func (s *BootlooseSuite) GetKubeConfig(node string, k0sKubeconfigArgs ...string) hostURL, err := url.Parse(cfg.Host) if err != nil { - return nil, fmt.Errorf("can't parse port value `%s`: %w", cfg.Host, err) + return nil, fmt.Errorf("can't parse Kubernetes API server host %q: %w", cfg.Host, err) } - port, err := strconv.ParseInt(hostURL.Port(), 10, 32) + port, err := strconv.ParseUint(hostURL.Port(), 10, 16) if err != nil { - return nil, fmt.Errorf("can't parse port value `%s`: %w", hostURL.Port(), err) + return nil, fmt.Errorf("can't parse Kubernetes API server port %q: %w", hostURL.Port(), err) } hostPort, err := machine.HostPort(int(port)) if err != nil { - return nil, fmt.Errorf("bootloose machine has to have %d port mapped: %w", port, err) + return nil, fmt.Errorf("can't find host port for Kubernetes API server port %d: %w", port, err) } cfg.Host = fmt.Sprintf("localhost:%d", hostPort) return cfg, nil diff --git a/pkg/apis/k0s/v1beta1/api.go b/pkg/apis/k0s/v1beta1/api.go index c70f25bc1565..620c499f76ad 100644 --- a/pkg/apis/k0s/v1beta1/api.go +++ b/pkg/apis/k0s/v1beta1/api.go @@ -17,12 +17,14 @@ limitations under the License. package v1beta1 import ( + "encoding/json" "fmt" "net" "github.com/k0sproject/k0s/internal/pkg/iface" "github.com/k0sproject/k0s/internal/pkg/stringslice" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "github.com/asaskevich/govalidator" @@ -32,37 +34,43 @@ var _ Validateable = (*APISpec)(nil) // APISpec defines the settings for the K0s API type APISpec struct { - // Local address on which to bind an API - Address string `json:"address"` + // Address on which to connect to the API server. + // +optional + Address string `json:"address,omitempty"` // The loadbalancer address (for k0s controllers running behind a loadbalancer) + // +optional ExternalAddress string `json:"externalAddress,omitempty"` + // Map of key-values (strings) for any extra arguments to pass down to Kubernetes api-server process + // +optional ExtraArgs map[string]string `json:"extraArgs,omitempty"` + // Custom port for k0s-api server to listen on (default: 9443) + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=9443 + // +optional K0sAPIPort int `json:"k0sApiPort,omitempty"` // Custom port for kube-api server to listen on (default: 6443) - Port int `json:"port"` + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=6443 + // +optional + Port int `json:"port,omitempty"` // List of additional addresses to push to API servers serving the certificate - SANs []string `json:"sans"` + // +optional + SANs []string `json:"sans,omitempty"` } -const defaultKasPort = 6443 - // DefaultAPISpec default settings for api func DefaultAPISpec() *APISpec { - // Collect all nodes addresses for sans - addresses, _ := iface.AllAddresses() - publicAddress, _ := iface.FirstPublicAddress() - return &APISpec{ - Port: defaultKasPort, - K0sAPIPort: 9443, - SANs: addresses, - Address: publicAddress, - ExtraArgs: make(map[string]string), - } + a := new(APISpec) + a.setDefaults() + a.SANs, _ = iface.AllAddresses() + return a } // APIAddress ... @@ -131,13 +139,47 @@ func (a *APISpec) Validate() []error { errors = append(errors, field.Invalid(path, san, "invalid IP address / DNS name")) } + if a.ExternalAddress != "" { + validateIPAddressOrDNSName(field.NewPath("externalAddress"), a.ExternalAddress) + } + + for _, msg := range validation.IsValidPortNum(a.K0sAPIPort) { + errors = append(errors, field.Invalid(field.NewPath("k0sApiPort"), a.K0sAPIPort, msg)) + } + + for _, msg := range validation.IsValidPortNum(a.Port) { + errors = append(errors, field.Invalid(field.NewPath("port"), a.Port, msg)) + } + sansPath := field.NewPath("sans") for idx, san := range a.SANs { validateIPAddressOrDNSName(sansPath.Index(idx), san) } - if a.ExternalAddress != "" { - validateIPAddressOrDNSName(field.NewPath("externalAddress"), a.ExternalAddress) - } return errors } + +// Sets in some sane defaults when unmarshaling the data from JSON. +func (a *APISpec) UnmarshalJSON(data []byte) error { + type apiSpec APISpec + jc := (*apiSpec)(a) + + if err := json.Unmarshal(data, jc); err != nil { + return err + } + + a.setDefaults() + return nil +} + +func (a *APISpec) setDefaults() { + if a.Address == "" { + a.Address, _ = iface.FirstPublicAddress() + } + if a.K0sAPIPort == 0 { + a.K0sAPIPort = 9443 + } + if a.Port == 0 { + a.Port = 6443 + } +} diff --git a/pkg/apis/k0s/v1beta1/api_test.go b/pkg/apis/k0s/v1beta1/api_test.go index d7551d14baaf..3337aef71e47 100644 --- a/pkg/apis/k0s/v1beta1/api_test.go +++ b/pkg/apis/k0s/v1beta1/api_test.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + "errors" "testing" "github.com/stretchr/testify/suite" @@ -30,22 +31,23 @@ func (s *APISuite) TestValidation() { s.T().Run("defaults_are_valid", func(t *testing.T) { a := DefaultAPISpec() - s.Nil(a.Validate()) + s.NoError(errors.Join(a.Validate()...)) }) s.T().Run("accepts_ipv6_as_address", func(t *testing.T) { - a := APISpec{ - Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", - } - - s.Nil(a.Validate()) + ipV6Addr := "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + a := APISpec{Address: ipV6Addr} + a.setDefaults() + s.Equal(ipV6Addr, a.Address) + s.NoError(errors.Join(a.Validate()...)) }) s.T().Run("invalid_api_address", func(t *testing.T) { a := APISpec{ Address: "something.that.is.not.valid//(())", } + a.setDefaults() errors := a.Validate() s.NotNil(errors) @@ -56,11 +58,11 @@ func (s *APISuite) TestValidation() { s.T().Run("invalid_sans_address", func(t *testing.T) { a := APISpec{ - Address: "1.2.3.4", SANs: []string{ "something.that.is.not.valid//(())", }, } + a.setDefaults() errors := a.Validate() s.NotNil(errors) diff --git a/pkg/apis/k0s/v1beta1/clusterconfig_types.go b/pkg/apis/k0s/v1beta1/clusterconfig_types.go index e5248d1422dd..7dfd99da5aa3 100644 --- a/pkg/apis/k0s/v1beta1/clusterconfig_types.go +++ b/pkg/apis/k0s/v1beta1/clusterconfig_types.go @@ -39,13 +39,13 @@ const ( // ClusterSpec defines the desired state of ClusterConfig type ClusterSpec struct { - API *APISpec `json:"api"` + API *APISpec `json:"api,omitempty"` ControllerManager *ControllerManagerSpec `json:"controllerManager,omitempty"` Scheduler *SchedulerSpec `json:"scheduler,omitempty"` - Storage *StorageSpec `json:"storage"` - Network *Network `json:"network"` + Storage *StorageSpec `json:"storage,omitempty"` + Network *Network `json:"network,omitempty"` WorkerProfiles WorkerProfiles `json:"workerProfiles,omitempty"` - Telemetry *ClusterTelemetry `json:"telemetry"` + Telemetry *ClusterTelemetry `json:"telemetry,omitempty"` Install *InstallSpec `json:"installConfig,omitempty"` Images *ClusterImages `json:"images,omitempty"` Extensions *ClusterExtensions `json:"extensions,omitempty"` diff --git a/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml b/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml index 92b445760278..c8533d90fed1 100644 --- a/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml +++ b/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml @@ -43,7 +43,7 @@ spec: description: APISpec defines the settings for the K0s API properties: address: - description: Local address on which to bind an API + description: Address on which to connect to the API server. type: string externalAddress: description: The loadbalancer address (for k0s controllers running @@ -56,12 +56,18 @@ spec: to pass down to Kubernetes api-server process type: object k0sApiPort: + default: 9443 description: 'Custom port for k0s-api server to listen on (default: 9443)' + maximum: 65535 + minimum: 1 type: integer port: + default: 6443 description: 'Custom port for kube-api server to listen on (default: 6443)' + maximum: 65535 + minimum: 1 type: integer sans: description: List of additional addresses to push to API servers