Skip to content

Commit

Permalink
Merge pull request #4335 from twz123/apispec-defaults
Browse files Browse the repository at this point in the history
Proper defaulting and validation for the API spec
  • Loading branch information
twz123 authored Apr 26, 2024
2 parents bd12eff + 2b2ab9c commit aa83b6f
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 35 deletions.
8 changes: 4 additions & 4 deletions inttest/common/bootloosesuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 61 additions & 19 deletions pkg/apis/k0s/v1beta1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 ...
Expand Down Expand Up @@ -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
}
}
16 changes: 9 additions & 7 deletions pkg/apis/k0s/v1beta1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1beta1

import (
"errors"
"testing"

"github.com/stretchr/testify/suite"
Expand All @@ -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)
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions pkg/apis/k0s/v1beta1/clusterconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit aa83b6f

Please sign in to comment.