diff --git a/glide.lock b/glide.lock index ca61b04ab..fd6df6b36 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ hash: eae225e9e9024cb6827e64e8124f5ab4ed973d6516ab0e1e00d066772db67168 -updated: 2018-10-08T10:09:56.361004325-04:00 +updated: 2018-10-16T09:33:03.819870676-04:00 imports: - name: github.com/beorn7/perks version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 @@ -74,7 +74,7 @@ imports: - name: github.com/modern-go/reflect2 version: 05fbef0ca5da472bbf96c9322b84a53edc03c9fd - name: github.com/openshift/api - version: b3641e9bb6985e9d38a96bf53f0e7e243acf2ccb + version: a46029836bdc207f9949fc5f4907f0bfd3cf7763 subpackages: - apps - apps/v1 @@ -121,7 +121,7 @@ imports: - name: github.com/openshift/client-go version: 431ec9a26e5021f35fa41ee9a89842db9bfdb370 - name: github.com/openshift/library-go - version: a045d08096d2578cde35812ad53cae0f4918c0ef + version: f61b6f9c40ee4457a53d547d90c6335d501d6f10 subpackages: - pkg/assets - pkg/config/client @@ -130,6 +130,7 @@ imports: - pkg/operator/resource/resourceapply - pkg/operator/resource/resourcemerge - pkg/operator/resource/resourceread + - pkg/operator/status - pkg/operator/v1alpha1helpers - pkg/operator/versioning - pkg/serviceability @@ -309,6 +310,7 @@ imports: subpackages: - discovery - discovery/fake + - dynamic - informers - informers/admissionregistration - informers/admissionregistration/v1alpha1 diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index ae6b67649..9ee0bde9c 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -5,6 +5,9 @@ import ( "os" "time" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/tools/cache" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -13,22 +16,29 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/openshift/cluster-openshift-controller-manager-operator/pkg/apis/openshiftcontrollermanager/v1alpha1" operatorconfigclient "github.com/openshift/cluster-openshift-controller-manager-operator/pkg/generated/clientset/versioned" operatorsv1alpha1client "github.com/openshift/cluster-openshift-controller-manager-operator/pkg/generated/clientset/versioned/typed/openshiftcontrollermanager/v1alpha1" operatorclientinformers "github.com/openshift/cluster-openshift-controller-manager-operator/pkg/generated/informers/externalversions" "github.com/openshift/cluster-openshift-controller-manager-operator/pkg/operator/v311_00_assets" + "github.com/openshift/library-go/pkg/operator/status" ) func RunOperator(clientConfig *rest.Config, stopCh <-chan struct{}) error { kubeClient, err := kubernetes.NewForConfig(clientConfig) if err != nil { - panic(err) + return err } operatorConfigClient, err := operatorconfigclient.NewForConfig(clientConfig) if err != nil { - panic(err) + return err + } + dynamicClient, err := dynamic.NewForConfig(clientConfig) + if err != nil { + return err } + ensureOperatorConfigExists(operatorConfigClient.OpenshiftcontrollermanagerV1alpha1(), "v3.11.0/openshift-controller-manager/operator-config.yaml") operatorConfigInformers := operatorclientinformers.NewSharedInformerFactory(operatorConfigClient, 10*time.Minute) kubeInformersNamespaced := informers.NewFilteredSharedInformerFactory(kubeClient, 10*time.Minute, targetNamespaceName, nil) @@ -40,12 +50,20 @@ func RunOperator(clientConfig *rest.Config, stopCh <-chan struct{}) error { kubeClient, ) - ensureOperatorConfigExists(operator.operatorConfigClient, "v3.11.0/openshift-controller-manager/operator-config.yaml") + clusterOperatorStatus := status.NewClusterOperatorStatusController( + "openshift-apiserver", + "openshift-apiserver", + dynamicClient, + &operatorStatusProvider{informers: operatorConfigInformers}, + ) operatorConfigInformers.Start(stopCh) kubeInformersNamespaced.Start(stopCh) - operator.Run(1, stopCh) + go operator.Run(1, stopCh) + go clusterOperatorStatus.Run(1, stopCh) + + <-stopCh return fmt.Errorf("stopped") } @@ -92,3 +110,20 @@ func ensureOperatorConfigExists(client operatorsv1alpha1client.OpenShiftControll } } } + +type operatorStatusProvider struct { + informers operatorclientinformers.SharedInformerFactory +} + +func (p *operatorStatusProvider) Informer() cache.SharedIndexInformer { + return p.informers.Openshiftcontrollermanager().V1alpha1().OpenShiftControllerManagerOperatorConfigs().Informer() +} + +func (p *operatorStatusProvider) CurrentStatus() (operatorv1alpha1.OperatorStatus, error) { + instance, err := p.informers.Openshiftcontrollermanager().V1alpha1().OpenShiftControllerManagerOperatorConfigs().Lister().Get("instance") + if err != nil { + return operatorv1alpha1.OperatorStatus{}, err + } + + return instance.Status.OperatorStatus, nil +} diff --git a/vendor/github.com/openshift/api/openshiftcontrolplane/v1/types.go b/vendor/github.com/openshift/api/openshiftcontrolplane/v1/types.go index d10a5132e..1a1741208 100644 --- a/vendor/github.com/openshift/api/openshiftcontrolplane/v1/types.go +++ b/vendor/github.com/openshift/api/openshiftcontrolplane/v1/types.go @@ -14,32 +14,32 @@ type OpenShiftAPIServerConfig struct { metav1.TypeMeta `json:",inline"` // provides the standard apiserver configuration - configv1.GenericAPIServerConfig `json:",inline" protobuf:"bytes,1,opt,name=genericAPIServerConfig"` + configv1.GenericAPIServerConfig `json:",inline"` // imagePolicyConfig feeds the image policy admission plugin - ImagePolicyConfig ImagePolicyConfig `json:"imagePolicyConfig" protobuf:"bytes,9,opt,name=imagePolicyConfig"` + ImagePolicyConfig ImagePolicyConfig `json:"imagePolicyConfig"` // projectConfig feeds an admission plugin - ProjectConfig ProjectConfig `json:"projectConfig" protobuf:"bytes,10,opt,name=projectConfig"` + ProjectConfig ProjectConfig `json:"projectConfig"` // routingConfig holds information about routing and route generation - RoutingConfig RoutingConfig `json:"routingConfig" protobuf:"bytes,11,opt,name=routingConfig"` + RoutingConfig RoutingConfig `json:"routingConfig"` // serviceAccountOAuthGrantMethod is used for determining client authorization for service account oauth client. // It must be either: deny, prompt, or "" - ServiceAccountOAuthGrantMethod GrantHandlerType `json:"serviceAccountOAuthGrantMethod" protobuf:"bytes,12,opt,name=serviceAccountOAuthGrantMethod,casttype=GrantHandlerType"` + ServiceAccountOAuthGrantMethod GrantHandlerType `json:"serviceAccountOAuthGrantMethod"` // jenkinsPipelineConfig holds information about the default Jenkins template // used for JenkinsPipeline build strategy. // TODO this needs to become a normal plugin config - JenkinsPipelineConfig JenkinsPipelineConfig `json:"jenkinsPipelineConfig" protobuf:"bytes,13,opt,name=jenkinsPipelineConfig"` + JenkinsPipelineConfig JenkinsPipelineConfig `json:"jenkinsPipelineConfig"` // cloudProviderFile points to the cloud config file // TODO this needs to become a normal plugin config - CloudProviderFile string `json:"cloudProviderFile" protobuf:"bytes,14,opt,name=cloudProviderFile"` + CloudProviderFile string `json:"cloudProviderFile"` // TODO this needs to be removed. - APIServerArguments map[string][]string `json:"apiServerArguments" protobuf:"bytes,14,rep,name=apiServerArguments"` + APIServerArguments map[string][]string `json:"apiServerArguments"` } type GrantHandlerType string @@ -58,36 +58,36 @@ type RoutingConfig struct { // subdomain is the suffix appended to $service.$namespace. to form the default route hostname // DEPRECATED: This field is being replaced by routers setting their own defaults. This is the // "default" route. - Subdomain string `json:"subdomain" protobuf:"bytes,1,opt,name=subdomain"` + Subdomain string `json:"subdomain"` } type ImagePolicyConfig struct { // maxImagesBulkImportedPerRepository controls the number of images that are imported when a user // does a bulk import of a Docker repository. This number is set low to prevent users from // importing large numbers of images accidentally. Set -1 for no limit. - MaxImagesBulkImportedPerRepository int `json:"maxImagesBulkImportedPerRepository" protobuf:"varint,1,opt,name=maxImagesBulkImportedPerRepository"` + MaxImagesBulkImportedPerRepository int `json:"maxImagesBulkImportedPerRepository"` // allowedRegistriesForImport limits the docker registries that normal users may import // images from. Set this list to the registries that you trust to contain valid Docker // images and that you want applications to be able to import from. Users with // permission to create Images or ImageStreamMappings via the API are not affected by // this policy - typically only administrators or system integrations will have those // permissions. - AllowedRegistriesForImport AllowedRegistries `json:"allowedRegistriesForImport" protobuf:"bytes,2,rep,name=allowedRegistriesForImport"` + AllowedRegistriesForImport AllowedRegistries `json:"allowedRegistriesForImport"` // internalRegistryHostname sets the hostname for the default internal image // registry. The value must be in "hostname[:port]" format. // For backward compatibility, users can still use OPENSHIFT_DEFAULT_REGISTRY // environment variable but this setting overrides the environment variable. - InternalRegistryHostname string `json:"internalRegistryHostname" protobuf:"bytes,3,opt,name=internalRegistryHostname"` + InternalRegistryHostname string `json:"internalRegistryHostname"` // externalRegistryHostname sets the hostname for the default external image // registry. The external hostname should be set only when the image registry // is exposed externally. The value is used in 'publicDockerImageRepository' // field in ImageStreams. The value must be in "hostname[:port]" format. - ExternalRegistryHostname string `json:"externalRegistryHostname" protobuf:"bytes,4,opt,name=externalRegistryHostname"` + ExternalRegistryHostname string `json:"externalRegistryHostname"` // additionalTrustedCA is a path to a pem bundle file containing additional CAs that // should be trusted during imagestream import. - AdditionalTrustedCA string `json:"additionalTrustedCA" protobuf:"bytes,5,opt,name=additionalTrustedCA"` + AdditionalTrustedCA string `json:"additionalTrustedCA"` } // AllowedRegistries represents a list of registries allowed for the image import. @@ -99,23 +99,23 @@ type RegistryLocation struct { // DomainName specifies a domain name for the registry // In case the registry use non-standard (80 or 443) port, the port should be included // in the domain name as well. - DomainName string `json:"domainName" protobuf:"bytes,1,opt,name=domainName"` + DomainName string `json:"domainName"` // Insecure indicates whether the registry is secure (https) or insecure (http) // By default (if not specified) the registry is assumed as secure. - Insecure bool `json:"insecure,omitempty" protobuf:"varint,2,opt,name=insecure"` + Insecure bool `json:"insecure,omitempty"` } type ProjectConfig struct { // defaultNodeSelector holds default project node label selector - DefaultNodeSelector string `json:"defaultNodeSelector" protobuf:"bytes,1,opt,name=defaultNodeSelector"` + DefaultNodeSelector string `json:"defaultNodeSelector"` // projectRequestMessage is the string presented to a user if they are unable to request a project via the projectrequest api endpoint - ProjectRequestMessage string `json:"projectRequestMessage" protobuf:"bytes,2,opt,name=projectRequestMessage"` + ProjectRequestMessage string `json:"projectRequestMessage"` // projectRequestTemplate is the template to use for creating projects in response to projectrequest. // It is in the format namespace/template and it is optional. // If it is not specified, a default template is used. - ProjectRequestTemplate string `json:"projectRequestTemplate" protobuf:"bytes,3,opt,name=projectRequestTemplate"` + ProjectRequestTemplate string `json:"projectRequestTemplate"` } // JenkinsPipelineConfig holds configuration for the Jenkins pipeline strategy @@ -123,17 +123,17 @@ type JenkinsPipelineConfig struct { // autoProvisionEnabled determines whether a Jenkins server will be spawned from the provided // template when the first build config in the project with type JenkinsPipeline // is created. When not specified this option defaults to true. - AutoProvisionEnabled *bool `json:"autoProvisionEnabled" protobuf:"varint,1,opt,name=autoProvisionEnabled"` + AutoProvisionEnabled *bool `json:"autoProvisionEnabled"` // templateNamespace contains the namespace name where the Jenkins template is stored - TemplateNamespace string `json:"templateNamespace" protobuf:"bytes,2,opt,name=templateNamespace"` + TemplateNamespace string `json:"templateNamespace"` // templateName is the name of the default Jenkins template - TemplateName string `json:"templateName" protobuf:"bytes,3,opt,name=templateName"` + TemplateName string `json:"templateName"` // serviceName is the name of the Jenkins service OpenShift uses to detect // whether a Jenkins pipeline handler has already been installed in a project. // This value *must* match a service name in the provided template. - ServiceName string `json:"serviceName" protobuf:"bytes,4,opt,name=serviceName"` + ServiceName string `json:"serviceName"` // parameters specifies a set of optional parameters to the Jenkins template. - Parameters map[string]string `json:"parameters" protobuf:"bytes,5,rep,name=parameters"` + Parameters map[string]string `json:"parameters"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/vendor/github.com/openshift/api/operator/v1alpha1/types.go b/vendor/github.com/openshift/api/operator/v1alpha1/types.go index cf9725e8e..92e8b46e1 100644 --- a/vendor/github.com/openshift/api/operator/v1alpha1/types.go +++ b/vendor/github.com/openshift/api/operator/v1alpha1/types.go @@ -26,6 +26,10 @@ type OperatorSpec struct { // imagePullSpec is the image to use for the component. ImagePullSpec string `json:"imagePullSpec"` + // imagePullPolicy specifies the image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, + // or IfNotPresent otherwise. + ImagePullPolicy string `json:"imagePullPolicy"` + // version is the desired state in major.minor.micro-patch. Usually patch is ignored. Version string `json:"version"` @@ -49,8 +53,13 @@ const ( ConditionFalse ConditionStatus = "False" ConditionUnknown ConditionStatus = "Unknown" - OperatorStatusTypeAvailable = "Available" - OperatorStatusTypeMigrating = "Migrating" + // these conditions match the conditions for the ClusterOperator type. + OperatorStatusTypeAvailable = "Available" + OperatorStatusTypeProgressing = "Progressing" + OperatorStatusTypeFailing = "Failing" + + OperatorStatusTypeMigrating = "Migrating" + // TODO this is going to be removed OperatorStatusTypeSyncSuccessful = "SyncSuccessful" ) diff --git a/vendor/github.com/openshift/api/operator/v1alpha1/types_swagger_doc_generated.go b/vendor/github.com/openshift/api/operator/v1alpha1/types_swagger_doc_generated.go index 068676efb..9907fed58 100644 --- a/vendor/github.com/openshift/api/operator/v1alpha1/types_swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/operator/v1alpha1/types_swagger_doc_generated.go @@ -76,6 +76,7 @@ var map_OperatorSpec = map[string]string{ "": "OperatorSpec contains common fields for an operator to need. It is intended to be anonymous included inside of the Spec struct for you particular operator.", "managementState": "managementState indicates whether and how the operator should manage the component", "imagePullSpec": "imagePullSpec is the image to use for the component.", + "imagePullPolicy": "imagePullPolicy specifies the image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise.", "version": "version is the desired state in major.minor.micro-patch. Usually patch is ignored.", "logging": "logging contains glog parameters for the component pods. It's always a command line arg for the moment", } diff --git a/vendor/github.com/openshift/library-go/.travis.yml b/vendor/github.com/openshift/library-go/.travis.yml index 7f4ea3a92..4f9031964 100644 --- a/vendor/github.com/openshift/library-go/.travis.yml +++ b/vendor/github.com/openshift/library-go/.travis.yml @@ -1,10 +1,10 @@ language: go go: - - 1.9 + - "1.10" install: - - go get -u github.com/golang/lint/golint + - go get -u golang.org/x/lint/golint script: - make verify build test diff --git a/vendor/github.com/openshift/library-go/glide.lock b/vendor/github.com/openshift/library-go/glide.lock index 44dce7678..0602cfca4 100644 --- a/vendor/github.com/openshift/library-go/glide.lock +++ b/vendor/github.com/openshift/library-go/glide.lock @@ -1,5 +1,5 @@ hash: 92ee9b295f6ff28dbd1eeaf1696d41cac71175665275d9047f1b9c53d5b2f01f -updated: 2018-10-08T09:10:47.664316504-04:00 +updated: 2018-10-16T09:26:46.106375778-04:00 imports: - name: github.com/blang/semver version: b38d23b8782a487059e8fc8773e9a5b228a77cb6 @@ -46,6 +46,10 @@ imports: version: 787624de3eb7bd915c329cba748687a3b22666a6 subpackages: - diskcache +- name: github.com/hashicorp/golang-lru + version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4 + subpackages: + - simplelru - name: github.com/imdario/mergo version: 6633656539c1639d9d78127b7d47c622b5d7b6dc - name: github.com/inconshreveable/mousetrap @@ -57,7 +61,7 @@ imports: - name: github.com/modern-go/reflect2 version: 05fbef0ca5da472bbf96c9322b84a53edc03c9fd - name: github.com/openshift/api - version: b3641e9bb6985e9d38a96bf53f0e7e243acf2ccb + version: a46029836bdc207f9949fc5f4907f0bfd3cf7763 subpackages: - apps - apps/v1 @@ -196,6 +200,7 @@ imports: - pkg/api/errors - pkg/api/meta - pkg/api/resource + - pkg/apis/meta/internalversion - pkg/apis/meta/v1 - pkg/apis/meta/v1/unstructured - pkg/apis/meta/v1beta1 @@ -213,6 +218,7 @@ imports: - pkg/runtime/serializer/versioning - pkg/selection - pkg/types + - pkg/util/cache - pkg/util/clock - pkg/util/diff - pkg/util/errors @@ -243,6 +249,7 @@ imports: subpackages: - discovery - discovery/fake + - dynamic - kubernetes - kubernetes/fake - kubernetes/scheme @@ -313,6 +320,7 @@ imports: - rest/watch - testing - tools/auth + - tools/cache - tools/clientcmd - tools/clientcmd/api - tools/clientcmd/api/latest @@ -320,14 +328,18 @@ imports: - tools/leaderelection - tools/leaderelection/resourcelock - tools/metrics + - tools/pager - tools/record - tools/reference - transport + - util/buffer - util/cert - util/connrotation - util/flowcontrol - util/homedir - util/integer + - util/retry + - util/workqueue - name: k8s.io/kube-aggregator version: 89cd614e9090a2f1e78316ed459857c49c55d276 subpackages: diff --git a/vendor/github.com/openshift/library-go/hack/verify-golint.sh b/vendor/github.com/openshift/library-go/hack/verify-golint.sh index 829baf37c..4c3fa05cc 100755 --- a/vendor/github.com/openshift/library-go/hack/verify-golint.sh +++ b/vendor/github.com/openshift/library-go/hack/verify-golint.sh @@ -2,7 +2,7 @@ if ! command -v golint > /dev/null; then echo 'Can not find golint, install with:' - echo 'go get -u github.com/golang/lint/golint' + echo 'go get -u golang.org/x/lint/golint' exit 1 fi diff --git a/vendor/github.com/openshift/library-go/pkg/config/configdefaults/config_default.go b/vendor/github.com/openshift/library-go/pkg/config/configdefaults/config_default.go index c0154203b..0bd77a7f4 100644 --- a/vendor/github.com/openshift/library-go/pkg/config/configdefaults/config_default.go +++ b/vendor/github.com/openshift/library-go/pkg/config/configdefaults/config_default.go @@ -1,6 +1,8 @@ package configdefaults import ( + "time" + configv1 "github.com/openshift/api/config/v1" "github.com/openshift/library-go/pkg/crypto" ) @@ -11,6 +13,18 @@ func DefaultString(target *string, defaultVal string) { } } +func DefaultInt(target *int, defaultVal int) { + if *target == 0 { + *target = defaultVal + } +} + +func DefaultMetaDuration(target *time.Duration, defaultVal time.Duration) { + if *target == 0 { + *target = defaultVal + } +} + func DefaultStringSlice(target *[]string, defaultVal []string) { if len(*target) == 0 { *target = defaultVal diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go new file mode 100644 index 000000000..d3de83967 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go @@ -0,0 +1,38 @@ +package resourceapply + +import ( + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + storageclientv1 "k8s.io/client-go/kubernetes/typed/storage/v1" + + "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" +) + +// ApplyStorageClass merges objectmeta, tries to write everything else +func ApplyStorageClass(client storageclientv1.StorageClassesGetter, required *storagev1.StorageClass) (*storagev1.StorageClass, bool, error) { + existing, err := client.StorageClasses().Get(required.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + actual, err := client.StorageClasses().Create(required) + return actual, true, err + } + if err != nil { + return nil, false, err + } + + modified := resourcemerge.BoolPtr(false) + resourcemerge.EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) + contentSame := equality.Semantic.DeepEqual(existing, required) + if contentSame && !*modified { + return existing, false, nil + } + + objectMeta := existing.ObjectMeta.DeepCopy() + existing = required.DeepCopy() + existing.ObjectMeta = *objectMeta + + // TODO if provisioner, parameters, reclaimpolicy, or volumebindingmode are different, update will fail so delete and recreate + actual, err := client.StorageClasses().Update(existing) + return actual, true, err +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceread/storage.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceread/storage.go new file mode 100644 index 000000000..3a488870e --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceread/storage.go @@ -0,0 +1,26 @@ +package resourceread + +import ( + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +var ( + storageScheme = runtime.NewScheme() + storageCodecs = serializer.NewCodecFactory(storageScheme) +) + +func init() { + if err := storagev1.AddToScheme(storageScheme); err != nil { + panic(err) + } +} + +func ReadStorageClassV1OrDie(objBytes []byte) *storagev1.StorageClass { + requiredObj, err := runtime.Decode(storageCodecs.UniversalDecoder(storagev1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return requiredObj.(*storagev1.StorageClass) +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/controller.go b/vendor/github.com/openshift/library-go/pkg/operator/status/controller.go new file mode 100644 index 000000000..4dbf67ffa --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/controller.go @@ -0,0 +1,206 @@ +package status + +import ( + "bytes" + "encoding/json" + "fmt" + "time" + + "github.com/golang/glog" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1" + "github.com/openshift/library-go/pkg/operator/v1alpha1helpers" +) + +var workQueueKey = "instance" + +type OperatorStatusProvider interface { + Informer() cache.SharedIndexInformer + CurrentStatus() (operatorv1alpha1.OperatorStatus, error) +} + +type StatusSyncer struct { + clusterOperatorNamespace string + clusterOperatorName string + + // TODO use a generated client when it moves to openshift/api + clusterOperatorClient dynamic.ResourceInterface + + operatorStatusProvider OperatorStatusProvider + + // queue only ever has one item, but it has nice error handling backoff/retry semantics + queue workqueue.RateLimitingInterface +} + +func NewClusterOperatorStatusController( + namespace, name string, + clusterOperatorClient dynamic.Interface, + operatorStatusProvider OperatorStatusProvider, +) *StatusSyncer { + c := &StatusSyncer{ + clusterOperatorNamespace: namespace, + clusterOperatorName: name, + clusterOperatorClient: clusterOperatorClient.Resource(schema.GroupVersionResource{Group: "operatorstatus.openshift.io", Version: "v1", Resource: "clusteroperators"}).Namespace(namespace), + operatorStatusProvider: operatorStatusProvider, + + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "StatusSyncer-"+name), + } + + operatorStatusProvider.Informer().AddEventHandler(c.eventHandler()) + // TODO watch clusterOperator.status changes when it moves to openshift/api + + return c +} + +// sync reacts to a change in prereqs by finding information that is required to match another value in the cluster. This +// must be information that is logically "owned" by another component. +func (c StatusSyncer) sync() error { + currentDetailedStatus, err := c.operatorStatusProvider.CurrentStatus() + if apierrors.IsNotFound(err) { + glog.Infof("operator.status not found") + return c.clusterOperatorClient.Delete(c.clusterOperatorName, nil) + } + if err != nil { + return err + } + + operatorConfig, err := c.clusterOperatorClient.Get(c.clusterOperatorName, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + if operatorConfig == nil { + glog.Infof("clusterOperator %s/%s not found", c.clusterOperatorNamespace, c.clusterOperatorName) + operatorConfig = &unstructured.Unstructured{Object: map[string]interface{}{}} + } + unstructured.RemoveNestedField(operatorConfig.Object, "status") + unstructured.SetNestedField(operatorConfig.Object, "ClusterOperator", "kind") + unstructured.SetNestedField(operatorConfig.Object, "operatorstatus.openshift.io/v1", "apiVersion") + unstructured.SetNestedField(operatorConfig.Object, c.clusterOperatorNamespace, "metadata", "namespace") + unstructured.SetNestedField(operatorConfig.Object, c.clusterOperatorName, "metadata", "name") + + errorMessages := []string{} + if currentDetailedStatus.TargetAvailability != nil { + errorMessages = append(errorMessages, currentDetailedStatus.TargetAvailability.Errors...) + } + if currentDetailedStatus.CurrentAvailability != nil { + unstructured.SetNestedField(operatorConfig.Object, currentDetailedStatus.CurrentAvailability.Version, "status", "version") + errorMessages = append(errorMessages, currentDetailedStatus.CurrentAvailability.Errors...) + } + + conditions := []interface{}{} + availableCondition, err := OperatorConditionToClusterOperatorCondition(v1alpha1helpers.FindOperatorCondition(currentDetailedStatus.Conditions, operatorv1alpha1.OperatorStatusTypeAvailable)) + if err != nil { + return err + } + if availableCondition != nil { + conditions = append(conditions, availableCondition) + } + failingCondition, err := OperatorConditionToClusterOperatorCondition(v1alpha1helpers.FindOperatorCondition(currentDetailedStatus.Conditions, operatorv1alpha1.OperatorStatusTypeFailing)) + if err != nil { + return err + } + if failingCondition != nil { + conditions = append(conditions, failingCondition) + } + progressingCondition, err := OperatorConditionToClusterOperatorCondition(v1alpha1helpers.FindOperatorCondition(currentDetailedStatus.Conditions, operatorv1alpha1.OperatorStatusTypeProgressing)) + if err != nil { + return err + } + if progressingCondition != nil { + conditions = append(conditions, failingCondition) + } + unstructured.SetNestedSlice(operatorConfig.Object, conditions, "status", "conditions") + + glog.V(4).Infof("clusterOperator %s/%s set to %v", c.clusterOperatorNamespace, c.clusterOperatorName, runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, operatorConfig)) + _, updateErr := c.clusterOperatorClient.Update(operatorConfig) + if apierrors.IsNotFound(updateErr) { + _, createErr := c.clusterOperatorClient.Create(operatorConfig) + if apierrors.IsNotFound(createErr) { + // this means that the API isn't present. We did not fail. Try again later + glog.Infof("ClusterOperator API not created") + c.queue.AddRateLimited(workQueueKey) + return nil + } + if createErr != nil { + return createErr + } + } + if updateErr != nil { + return updateErr + } + + return nil +} + +func OperatorConditionToClusterOperatorCondition(condition *operatorv1alpha1.OperatorCondition) (map[string]interface{}, error) { + if condition == nil { + return nil, nil + } + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(condition); err != nil { + return nil, err + } + ret := map[string]interface{}{} + if err := json.NewDecoder(buf).Decode(&ret); err != nil { + return nil, err + } + + return ret, nil +} + +func (c *StatusSyncer) Run(workers int, stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + defer c.queue.ShutDown() + + glog.Infof("Starting StatusSyncer-" + c.clusterOperatorName) + defer glog.Infof("Shutting down StatusSyncer-" + c.clusterOperatorName) + + // doesn't matter what workers say, only start one. + go wait.Until(c.runWorker, time.Second, stopCh) + + <-stopCh +} + +func (c *StatusSyncer) runWorker() { + for c.processNextWorkItem() { + } +} + +func (c *StatusSyncer) processNextWorkItem() bool { + dsKey, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(dsKey) + + err := c.sync() + if err == nil { + c.queue.Forget(dsKey) + return true + } + + utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err)) + c.queue.AddRateLimited(dsKey) + + return true +} + +// eventHandler queues the operator to check spec and status +func (c *StatusSyncer) eventHandler() cache.ResourceEventHandler { + return cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { c.queue.Add(workQueueKey) }, + UpdateFunc: func(old, new interface{}) { c.queue.Add(workQueueKey) }, + DeleteFunc: func(obj interface{}) { c.queue.Add(workQueueKey) }, + } +}