Skip to content

Commit

Permalink
Add preflight checks (#250)
Browse files Browse the repository at this point in the history
* Add preflight checks

Signed-off-by: Bowen Zhu <[email protected]>

* Add Copyright and fix version

Signed-off-by: Bowen Zhu <[email protected]>

* Restructure preflight and get context from global flag --context

Signed-off-by: Bowen Zhu <[email protected]>
  • Loading branch information
promacanthus authored Jul 25, 2022
1 parent 796024a commit 7a41de5
Show file tree
Hide file tree
Showing 232 changed files with 23,755 additions and 10 deletions.
22 changes: 13 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
[comment]: # ( Copyright Contributors to the Open Cluster Management project )**Table of Contents**

- [Contributing guidelines](#contributing-guidelines)
- [Terms](#terms)
- [Certificate of Origin](#certificate-of-origin)
- [Contributing a patch](#contributing-a-patch)
- [Issue and pull request management](#issue-and-pull-request-management)
- [Requirements](#requirements)
- [Develop new commands](#Develop-new-commands)
- [Terms](#terms)
- [Certificate of Origin](#certificate-of-origin)
- [Contributing a patch](#contributing-a-patch)
- [Issue and pull request management](#issue-and-pull-request-management)
- [Requirements](#requirements)
- [Develop new commands](#develop-new-commands)
- [Resources](#resources)
- [Client](#client)
- [Unit tests](#unit-tests)
- [E2E tests](#e2e-tests)

# Contributing guidelines

Expand All @@ -27,6 +31,7 @@ By contributing to this project, you agree to the Developer Certificate of Origi
## Issue and pull request management

Anyone can comment on issues and submit reviews for pull requests. In order to be assigned an issue or pull request, you can leave a `/assign <your Github ID>` comment on the issue or pull request.

# Requirements

- Go 1.17
Expand All @@ -43,18 +48,17 @@ clusteradm <cmd> [subcmd] [flags]
- Each command must support the flag `--dry-run`.
- The command uses [klog V2](https://github.com/kubernetes/klog) as logging package. All messages must be using `klog.V(x)`, in rare exception `klog.Error` and `klog.Warning` can be used.


## Resources

- Some commands needs resources files, in the project uses the `Go 1.17` `go:embed` functionality to store the resources files.
- Each command package contains its own resources in the scenario package. The scenario package contains one go file which provides the `go:embed` `embed.FS` files.
- Each command package contains its own resources in the scenario package. The scenario package contains one go file which provides the `go:embed` `embed.FS` files.

## Client

- The [main](cmd/clusteradm.go) provides a cmdutil.Factory which can be leveraged to get different clients and also the *rest.Config. The factory can be passed to the cobra.Command and then save in the Options.

```Go
kubeClient, err := o.factory.KubernetesClientSet()
kubeClient, err := o.factory.KubernetesClientSet()
```

```Go
Expand Down
1 change: 1 addition & 0 deletions cmd/clusteradm/clusteradm.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func main() {

clusteradmFlags := genericclioptionsclusteradm.NewClusteradmFlags(f)
clusteradmFlags.AddFlags(flags)
clusteradmFlags.SetContext(kubeConfigFlags.Context)

// From this point and forward we get warnings on flags that contain "_" separators

Expand Down
24 changes: 24 additions & 0 deletions pkg/cmd/init/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"open-cluster-management.io/clusteradm/pkg/cmd/init/preflight"
"open-cluster-management.io/clusteradm/pkg/cmd/init/scenario"
"open-cluster-management.io/clusteradm/pkg/helpers"
"open-cluster-management.io/clusteradm/pkg/helpers/apply"
Expand Down Expand Up @@ -51,6 +53,28 @@ func (o *Options) validate() error {
if o.force {
return nil
}
// preflight check
f := o.ClusteradmFlags.KubectlFactory
kubeClient, _, _, err := helpers.GetClients(f)
if err != nil {
return err
}
if err := preflight.RunChecks(
[]preflight.Checker{
preflight.HubApiServerCheck{
ClusterCtx: o.ClusteradmFlags.Context,
ConfigPath: "", // TODO(@Promacanthus): user custom kubeconfig path from command line arguments.
},
preflight.ClusterInfoCheck{
Namespace: metav1.NamespacePublic,
ResourceName: preflight.BootstrapConfigMap,
ClusterCtx: o.ClusteradmFlags.Context,
ConfigPath: "", // TODO(@Promacanthus): user custom kubeconfig path from command line arguments.
Client: kubeClient,
},
}, os.Stderr); err != nil {
return err
}
restConfig, err := o.ClusteradmFlags.KubectlFactory.ToRESTConfig()
if err != nil {
return err
Expand Down
183 changes: 183 additions & 0 deletions pkg/cmd/init/preflight/checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright Contributors to the Open Cluster Management project
package preflight

import (
"bytes"
"context"
"fmt"
"io"
"net"
"net/url"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

var BootstrapConfigMap = "cluster-info"

type Error struct {
Msg string
}

func (e Error) Error() string {
return fmt.Sprintf("[preflight] Some fatal errors occurred:\n%s", e.Msg)
}

func (e *Error) Preflight() bool {
return true
}

// Checker validates the state of the cluster to ensure
// clusteradm will be successfully as often as possible.
type Checker interface {
Check() (warnings, errorList []error)
Name() string
}

type HubApiServerCheck struct {
ClusterCtx string // current-context in kubeconfig
ConfigPath string // kubeconfig file path
}

func (c HubApiServerCheck) Check() (warnings []error, errorList []error) {
cluster, err := loadCurrentCluster(c.ClusterCtx, c.ConfigPath)
if err != nil {
return nil, []error{err}
}
u, err := url.Parse(cluster.Server)
if err != nil {
return nil, []error{err}
}
host, _, err := net.SplitHostPort(u.Host)
if err != nil {
return nil, []error{err}
}
if net.ParseIP(host) == nil {
return []error{errors.New("Hub Api Server is a domain name, maybe you should set HostAlias in klusterlet")}, nil
}
return nil, nil
}

func (c HubApiServerCheck) Name() string {
return "HubApiServer check"
}

// ClusterInfoCheck checks whether the target kubernetes resource exist in the cluster.
type ClusterInfoCheck struct {
Namespace string
ResourceName string
ClusterCtx string // current-context in kubeconfig
ConfigPath string // kubeconfig file path
Client kubernetes.Interface
}

func (c ClusterInfoCheck) Check() (warnings []error, errorList []error) {
cm, err := c.Client.CoreV1().ConfigMaps(c.Namespace).Get(context.Background(), c.ResourceName, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
resourceNotFound := errors.New("no ConfigMap named cluster-info in the kube-public namespace, clusteradm will creates it")
cluster, err := loadCurrentCluster(c.ClusterCtx, c.ConfigPath)
if err != nil {
return []error{resourceNotFound}, []error{err}
}
if err := createClusterInfo(c.Client, cluster); err != nil {
return []error{resourceNotFound}, []error{err}
}
return []error{resourceNotFound}, nil
}
return nil, []error{err}
}
if len(cm.Data["kubeconfig"]) == 0 {
return nil, []error{errors.New("empty kubeconfig data in cluster-info")}
}
return nil, nil
}

func (c ClusterInfoCheck) Name() string {
return "cluster-info check"
}

// loadCurrentCluster will load kubeconfig from file and return the current cluster.
// The default file path is ~/.kube/config.
func loadCurrentCluster(context string, kubeConfigFilePath string) (*api.Cluster, error) {
var (
currentConfig *clientcmdapi.Config
err error
)

if len(kubeConfigFilePath) == 0 {
currentConfig, err = clientcmd.NewDefaultClientConfigLoadingRules().Load()
if err != nil {
return nil, err
}
} else {
currentConfig, err = clientcmd.LoadFromFile(kubeConfigFilePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to load kubeconfig file %s that already exists on disk", kubeConfigFilePath)
}
}
// load kubeconfig from file
// get the hub cluster context
if len(context) == 0 {
// use the current context from the kubeconfig
context = currentConfig.CurrentContext
}
currentCtx, exists := currentConfig.Contexts[context]
if !exists {
return nil, errors.Errorf("failed to find the given Current Context in Contexts of the kubeconfig file %s", kubeConfigFilePath)
}
currentCluster, exists := currentConfig.Clusters[currentCtx.Cluster]
if !exists {
return nil, errors.Errorf("failed to find the given CurrentContext Cluster in Clusters of the kubeconfig file %s", kubeConfigFilePath)
}
return currentCluster, nil
}

// createClusterInfo will create a ConfigMap named cluster-info in the kube-public namespace.
func createClusterInfo(client kubernetes.Interface, cluster *clientcmdapi.Cluster) error {
kubeconfig := &clientcmdapi.Config{Clusters: map[string]*clientcmdapi.Cluster{"": cluster}}
if err := clientcmdapi.FlattenConfig(kubeconfig); err != nil {
return err
}
kubeconfigBytes, err := clientcmd.Write(*kubeconfig)
if err != nil {
return err
}
clusterInfo := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: BootstrapConfigMap,
Namespace: metav1.NamespacePublic,
},
Immutable: BoolPointer(true),
Data: map[string]string{
"kubeconfig": string(kubeconfigBytes),
},
}
return CreateOrUpdateConfigMap(client, clusterInfo)
}

// RunChecks runs each check, display it's warning/errors,
// and once all are processed will exist if any errors occured.
func RunChecks(checks []Checker, ww io.Writer) error {
var errsBuffer bytes.Buffer
for _, check := range checks {
name := check.Name()
warnings, errs := check.Check()
for _, warning := range warnings {
_, _ = io.WriteString(ww, fmt.Sprintf("\t[WARNING %s]: %v\n", name, warning))
}
for _, err := range errs {
_, _ = errsBuffer.WriteString(fmt.Sprintf("\t[ERROR %s]: %v\n", name, err.Error()))
}
}
if errsBuffer.Len() > 0 {
return &Error{Msg: errsBuffer.String()}
}
return nil
}
Loading

0 comments on commit 7a41de5

Please sign in to comment.