Path: blob/main/install/installer/pkg/cluster/checks.go
2501 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cluster56import (7"context"8"fmt"9"net"10"net/netip"11"strings"1213"github.com/Masterminds/semver"14certmanager "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"15corev1 "k8s.io/api/core/v1"16"k8s.io/apimachinery/pkg/api/errors"17metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"18"k8s.io/client-go/rest"19)2021const (22// Allow pre-release range as GCP (and potentially others) use the patch for23// additional information, which get interpreted as pre-release (eg, 1.2.3-rc4)24kernelVersionConstraint = ">= 5.4.0-0"25kubernetesVersionConstraint = ">= 1.21.0-0"26)2728// checkCertManagerInstalled checks that cert-manager is installed as a cluster dependency29func checkCertManagerInstalled(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {30client, err := certmanager.NewForConfig(config)31if err != nil {32return nil, err33}3435clusterIssuers, err := client.CertmanagerV1().ClusterIssuers().List(ctx, metav1.ListOptions{})36if err != nil {37// If cert-manager not installed, this will error38return []ValidationError{39{40Message: err.Error(),41Type: ValidationStatusError,42},43}, nil44}4546if len(clusterIssuers.Items) == 0 {47// Treat as warning - may be bringing their own certs48return []ValidationError{49{50Message: "no cluster issuers configured",51Type: ValidationStatusWarning,52},53}, nil54}5556return nil, nil57}5859// checkContainerDRuntime checks that the nodes are running with the containerd runtime60func checkContainerDRuntime(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {61nodes, err := ListNodesFromContext(ctx, config)62if err != nil {63return nil, err64}6566var res []ValidationError67for _, node := range nodes {68runtime := node.Status.NodeInfo.ContainerRuntimeVersion69if !strings.Contains(runtime, "containerd") {70res = append(res, ValidationError{71Message: "container runtime not containerd on node: " + node.Name + ", runtime: " + runtime,72Type: ValidationStatusError,73})74}75}7677return res, nil78}7980func checkKubernetesVersion(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {81// Allow pre-releases in case provider appends anything to the version82constraint, err := semver.NewConstraint(kubernetesVersionConstraint)83if err != nil {84return nil, err85}8687server, err := serverVersion(ctx, config)88if err != nil {89return nil, err90}9192var res []ValidationError9394serverVersion, err := semver.NewVersion(server.GitVersion)95if err != nil {96res = append(res, ValidationError{97Message: err.Error() + " Kubernetes version: " + server.GitVersion,98Type: ValidationStatusWarning,99})100}101valid := constraint.Check(serverVersion)102if !valid {103res = append(res, ValidationError{104Message: "Kubernetes version " + server.GitVersion + " does not satisfy " + kubernetesVersionConstraint,105Type: ValidationStatusError,106})107}108109nodes, err := ListNodesFromContext(ctx, config)110if err != nil {111return nil, err112}113for _, node := range nodes {114kubeletVersion := node.Status.NodeInfo.KubeletVersion115116version, err := semver.NewVersion(kubeletVersion)117if err != nil {118// This means that the given version doesn't conform to semver format - user must decide119res = append(res, ValidationError{120Message: err.Error() + " Kubernetes version: " + kubeletVersion,121Type: ValidationStatusWarning,122})123break124}125126valid := constraint.Check(version)127128if !valid {129res = append(res, ValidationError{130Message: "Kubelet version " + kubeletVersion + " does not satisfy " + kubernetesVersionConstraint + " on node: " + node.Name,131Type: ValidationStatusError,132})133}134}135136return res, nil137}138139type checkSecretOpts struct {140RequiredFields []string141RecommendedFields []string142Validator func(*corev1.Secret) ([]ValidationError, error)143}144145type CheckSecretOpt func(*checkSecretOpts)146147func CheckSecretRequiredData(entries ...string) CheckSecretOpt {148return func(cso *checkSecretOpts) {149cso.RequiredFields = append(cso.RequiredFields, entries...)150}151}152153func CheckSecretRecommendedData(entries ...string) CheckSecretOpt {154return func(cso *checkSecretOpts) {155cso.RecommendedFields = append(cso.RecommendedFields, entries...)156}157}158159func CheckSecretRule(validator func(*corev1.Secret) ([]ValidationError, error)) CheckSecretOpt {160return func(cso *checkSecretOpts) {161cso.Validator = validator162}163}164165// CheckSecret produces a new check for an in-cluster secret166func CheckSecret(name string, opts ...CheckSecretOpt) ValidationCheck {167var cfg checkSecretOpts168for _, o := range opts {169o(&cfg)170}171172return ValidationCheck{173Name: name + " is present and valid",174Description: "ensures the " + name + " secret is present and contains the required data",175Check: func(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {176client, err := clientsetFromContext(ctx, config)177if err != nil {178return nil, err179}180181secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})182if errors.IsNotFound(err) {183return []ValidationError{184{185Message: "secret " + name + " not found",186Type: ValidationStatusError,187},188}, nil189} else if err != nil {190return nil, err191}192193var res []ValidationError194for _, k := range cfg.RequiredFields {195_, ok := secret.Data[k]196if !ok {197res = append(res, ValidationError{198Message: fmt.Sprintf("secret %s has no %s entry", name, k),199Type: ValidationStatusError,200})201}202}203for _, k := range cfg.RecommendedFields {204_, ok := secret.Data[k]205if !ok {206res = append(res, ValidationError{207Message: fmt.Sprintf("secret %s has no %s entry", name, k),208Type: ValidationStatusWarning,209})210}211}212213if cfg.Validator != nil {214vres, err := cfg.Validator(secret)215if err != nil {216return nil, err217}218res = append(res, vres...)219}220221return res, nil222},223}224}225226// checkKernelVersion checks the nodes are using the correct linux Kernel version227func checkKernelVersion(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {228constraint, err := semver.NewConstraint(kernelVersionConstraint)229if err != nil {230return nil, err231}232233nodes, err := ListNodesFromContext(ctx, config)234if err != nil {235return nil, err236}237238var res []ValidationError239for _, node := range nodes {240kernelVersion := node.Status.NodeInfo.KernelVersion241// Some GCP kernel versions contain a non-semver compatible suffix242kernelVersion = strings.TrimSuffix(kernelVersion, "+")243version, err := semver.NewVersion(kernelVersion)244if err != nil {245// This means that the given version doesn't conform to semver format - user must decide246res = append(res, ValidationError{247Message: err.Error() + " kernel version: " + kernelVersion,248Type: ValidationStatusWarning,249})250break251}252253valid := constraint.Check(version)254255if !valid {256res = append(res, ValidationError{257Message: "kernel version " + kernelVersion + " does not satisfy " + kernelVersionConstraint + " on node: " + node.Name,258Type: ValidationStatusError,259})260}261}262263return res, nil264}265266func checkNamespaceExists(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {267client, err := clientsetFromContext(ctx, config)268if err != nil {269return nil, err270}271272namespaces, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})273if err != nil {274return nil, err275}276namespaceExists := false277for _, nsp := range namespaces.Items {278if !namespaceExists && nsp.Name == namespace {279namespaceExists = true280break281}282}283284if !namespaceExists {285return []ValidationError{286{287Message: fmt.Sprintf("Namespace %s does not exist", namespace),288Type: ValidationStatusError,289},290}, nil291}292293return nil, nil294}295296func CheckWorkspaceCIDR(networkCIDR string) ValidationCheck {297return ValidationCheck{298Name: "workspace CIDR is present and valid",299Description: "ensures the workspace CIDR contains a valid network address range",300Check: func(ctx context.Context, config *rest.Config, namespace string) ([]ValidationError, error) {301netIP, ipNet, err := net.ParseCIDR(networkCIDR)302if err != nil {303return []ValidationError{304{305Message: fmt.Sprintf("invalid workspace CIDR: %v", err),306Type: ValidationStatusError,307},308}, nil309}310311ipNet.Mask.Size()312mask, _ := ipNet.Mask.Size()313if mask > 30 {314return []ValidationError{315{316Message: "the workspace CIDR does not have a mask less than or equal to /30",317Type: ValidationStatusError,318},319}, nil320}321322addr, err := netip.ParseAddr(netIP.String())323if err != nil {324return []ValidationError{325{326Message: fmt.Sprintf("invalid workspace CIDR: %v", err),327Type: ValidationStatusError,328},329}, nil330}331332vethIp := addr.Next()333if !vethIp.IsValid() {334return []ValidationError{335{336Message: fmt.Sprintf("workspace CIDR is not big enough (%v)", networkCIDR),337Type: ValidationStatusError,338},339}, nil340}341342cethIp := vethIp.Next()343if !cethIp.IsValid() {344return []ValidationError{345{346Message: fmt.Sprintf("workspace CIDR is not big enough (%v)", networkCIDR),347Type: ValidationStatusError,348},349}, nil350}351352return nil, nil353},354}355}356357358