forked from rook/rook
331 lines
8.5 KiB
Go
331 lines
8.5 KiB
Go
/*
|
|
Copyright 2016 The Rook Authors. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
Some of the code below came from https://github.com/coreos/etcd-operator
|
|
which also has the apache 2.0 license.
|
|
*/
|
|
package operator
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"k8s.io/client-go/kubernetes"
|
|
|
|
"k8s.io/client-go/rest"
|
|
|
|
"github.com/rook/rook/pkg/cephmgr/client"
|
|
"github.com/rook/rook/pkg/operator/k8sutil"
|
|
"k8s.io/client-go/pkg/api"
|
|
"k8s.io/client-go/pkg/api/unversioned"
|
|
"k8s.io/client-go/pkg/runtime"
|
|
"k8s.io/client-go/pkg/runtime/serializer"
|
|
kwatch "k8s.io/client-go/pkg/watch"
|
|
)
|
|
|
|
const (
|
|
initRetryDelay = 10 * time.Second
|
|
)
|
|
|
|
var (
|
|
ErrVersionOutdated = errors.New("requested version is outdated in apiserver")
|
|
)
|
|
|
|
const (
|
|
defaultPool = "rook"
|
|
)
|
|
|
|
type Operator struct {
|
|
Namespace string
|
|
MasterHost string
|
|
clientset *kubernetes.Clientset
|
|
waitCluster sync.WaitGroup
|
|
factory client.ConnectionFactory
|
|
stopChMap map[string]chan struct{}
|
|
clusters map[string]*Cluster
|
|
kubeHttpCli *http.Client
|
|
// Kubernetes resource version of the clusters
|
|
clusterRVs map[string]string
|
|
}
|
|
|
|
func New(host, namespace string, factory client.ConnectionFactory, clientset *kubernetes.Clientset) *Operator {
|
|
return &Operator{
|
|
Namespace: namespace,
|
|
MasterHost: host,
|
|
factory: factory,
|
|
clientset: clientset,
|
|
clusters: make(map[string]*Cluster),
|
|
clusterRVs: make(map[string]string),
|
|
stopChMap: map[string]chan struct{}{},
|
|
}
|
|
}
|
|
|
|
func (o *Operator) Run() error {
|
|
var watchVersion string
|
|
var err error
|
|
for {
|
|
watchVersion, err = o.initResources()
|
|
if err == nil {
|
|
break
|
|
}
|
|
logger.Errorf("failed to create tpr. %+v. retrying...", err)
|
|
<-time.After(initRetryDelay)
|
|
}
|
|
|
|
// watch for changes to the rook clusters
|
|
return o.watchTPR(watchVersion)
|
|
}
|
|
|
|
func (o *Operator) initResources() (string, error) {
|
|
httpCli, err := newHttpClient()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get tpr client. %+v", err)
|
|
}
|
|
o.kubeHttpCli = httpCli.Client
|
|
|
|
err = o.createTPR()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create TPR. %+v", err)
|
|
}
|
|
|
|
// Check if there is an existing cluster to recover
|
|
watchVersion, err := o.findAllClusters()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to find clusters. %+v", err)
|
|
}
|
|
return watchVersion, nil
|
|
}
|
|
|
|
func (o *Operator) watchTPR(watchVersion string) error {
|
|
logger.Infof("start watching rook tpr: %s", watchVersion)
|
|
defer func() {
|
|
for _, stop := range o.stopChMap {
|
|
close(stop)
|
|
}
|
|
o.waitCluster.Wait()
|
|
}()
|
|
|
|
eventCh, errCh := o.watch(watchVersion)
|
|
|
|
go func() {
|
|
pt := k8sutil.NewPanicTimer(time.Minute, "unexpected long blocking (> 1 Minute) when handling cluster event")
|
|
|
|
for event := range eventCh {
|
|
pt.Start()
|
|
|
|
c := event.Object
|
|
|
|
switch event.Type {
|
|
case kwatch.Added:
|
|
ns := c.Spec.Namespace
|
|
if ns == "" {
|
|
logger.Errorf("missing namespace attribute in rook spec")
|
|
continue
|
|
}
|
|
|
|
if len(o.stopChMap) > 0 {
|
|
logger.Warningf("only a single rook cluster is currently supported. Ignoring cluster for namespace %s", ns)
|
|
} else {
|
|
newCluster := newCluster(ns, c.Spec.Version, c.Spec.UseAllDevices, o.factory, o.clientset)
|
|
stopCh := make(chan struct{})
|
|
o.stopChMap[ns] = stopCh
|
|
o.clusters[ns] = newCluster
|
|
o.clusterRVs[ns] = c.Metadata.ResourceVersion
|
|
|
|
logger.Infof("starting new cluster %s in namespace %s", c.Metadata.Name, ns)
|
|
go startCluster(newCluster)
|
|
}
|
|
|
|
case kwatch.Modified:
|
|
logger.Infof("modifying a cluster not implemented")
|
|
|
|
case kwatch.Deleted:
|
|
logger.Infof("deleting a cluster not implemented")
|
|
}
|
|
|
|
pt.Stop()
|
|
}
|
|
}()
|
|
return <-errCh
|
|
|
|
}
|
|
|
|
func startCluster(cluster *Cluster) {
|
|
err := cluster.CreateInstance()
|
|
if err != nil {
|
|
logger.Errorf("failed to create cluster in namespace %s. %+v", cluster.Namespace, err)
|
|
}
|
|
}
|
|
|
|
func (o *Operator) findAllClusters() (string, error) {
|
|
logger.Info("finding existing clusters...")
|
|
clusterList, err := getClusterList(o.clientset.CoreV1().RESTClient(), o.Namespace)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
logger.Infof("found %d clusters", len(clusterList.Items))
|
|
for i := range clusterList.Items {
|
|
c := clusterList.Items[i]
|
|
|
|
stopCh := make(chan struct{})
|
|
ns := c.Spec.Namespace
|
|
existingCluster := newCluster(ns, c.Spec.Version, c.Spec.UseAllDevices, o.factory, o.clientset)
|
|
o.stopChMap[ns] = stopCh
|
|
o.clusters[ns] = existingCluster
|
|
o.clusterRVs[ns] = c.Metadata.ResourceVersion
|
|
|
|
logger.Infof("resuming cluster %s in namespace %s", c.Metadata.Name, ns)
|
|
go startCluster(existingCluster)
|
|
}
|
|
|
|
return clusterList.Metadata.ResourceVersion, nil
|
|
}
|
|
|
|
// watch creates a go routine, and watches the cluster.rook kind resources from
|
|
// the given watch version. It emits events on the resources through the returned
|
|
// event chan. Errors will be reported through the returned error chan. The go routine
|
|
// exits on any error.
|
|
func (o *Operator) watch(watchVersion string) (<-chan *Event, <-chan error) {
|
|
eventCh := make(chan *Event)
|
|
// On unexpected error case, the operator should exit
|
|
errCh := make(chan error, 1)
|
|
|
|
go func() {
|
|
defer close(eventCh)
|
|
|
|
for {
|
|
resp, err := watchClusters(o.MasterHost, o.Namespace, o.kubeHttpCli, watchVersion)
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
resp.Body.Close()
|
|
errCh <- errors.New("invalid status code: " + resp.Status)
|
|
return
|
|
}
|
|
|
|
decoder := json.NewDecoder(resp.Body)
|
|
for {
|
|
ev, st, err := pollEvent(decoder)
|
|
if err != nil {
|
|
if err == io.EOF { // apiserver will close stream periodically
|
|
logger.Debug("apiserver closed stream")
|
|
break
|
|
}
|
|
|
|
logger.Errorf("received invalid event from API server: %v", err)
|
|
errCh <- err
|
|
return
|
|
}
|
|
|
|
if st != nil {
|
|
resp.Body.Close()
|
|
|
|
if st.Code == http.StatusGone {
|
|
// event history is outdated.
|
|
// if nothing has changed, we can go back to watch again.
|
|
clusterList, err := getClusterList(o.clientset.CoreV1().RESTClient(), o.Namespace)
|
|
if err == nil && !o.isClustersCacheStale(clusterList.Items) {
|
|
watchVersion = clusterList.Metadata.ResourceVersion
|
|
break
|
|
}
|
|
|
|
// if anything has changed (or error on relist), we have to rebuild the state.
|
|
// go to recovery path
|
|
errCh <- ErrVersionOutdated
|
|
return
|
|
}
|
|
|
|
logger.Errorf("unexpected status response from API server: %v", st.Message)
|
|
break
|
|
}
|
|
|
|
logger.Debugf("rook cluster event: %+v", ev)
|
|
|
|
watchVersion = ev.Object.Metadata.ResourceVersion
|
|
eventCh <- ev
|
|
}
|
|
|
|
resp.Body.Close()
|
|
}
|
|
}()
|
|
|
|
return eventCh, errCh
|
|
}
|
|
|
|
func (o *Operator) isClustersCacheStale(currentClusters []Cluster) bool {
|
|
if len(o.clusterRVs) != len(currentClusters) {
|
|
return true
|
|
}
|
|
|
|
for _, cc := range currentClusters {
|
|
rv, ok := o.clusterRVs[cc.Metadata.Name]
|
|
if !ok || rv != cc.Metadata.ResourceVersion {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func watchClusters(host, ns string, httpClient *http.Client, resourceVersion string) (*http.Response, error) {
|
|
return httpClient.Get(fmt.Sprintf("%s/apis/%s/%s/namespaces/%s/clusters?watch=true&resourceVersion=%s",
|
|
host, tprGroup, tprVersion, ns, resourceVersion))
|
|
}
|
|
|
|
func getClusterList(restcli rest.Interface, ns string) (*ClusterList, error) {
|
|
b, err := restcli.Get().RequestURI(listClustersURI(ns)).DoRaw()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clusters := &ClusterList{}
|
|
if err := json.Unmarshal(b, clusters); err != nil {
|
|
return nil, err
|
|
}
|
|
return clusters, nil
|
|
}
|
|
|
|
func listClustersURI(ns string) string {
|
|
return fmt.Sprintf("/apis/%s/%s/namespaces/%s/clusters", tprGroup, tprVersion, ns)
|
|
}
|
|
|
|
func newHttpClient() (*rest.RESTClient, error) {
|
|
config, err := rest.InClusterConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config.GroupVersion = &unversioned.GroupVersion{
|
|
Group: tprGroup,
|
|
Version: tprVersion,
|
|
}
|
|
config.APIPath = "/apis"
|
|
config.ContentType = runtime.ContentTypeJSON
|
|
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: api.Codecs}
|
|
|
|
restcli, err := rest.RESTClientFor(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return restcli, nil
|
|
}
|