From dc20850374b2e35ce38d1dbedd08e9ea6ee59d97 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 8 Jan 2019 15:53:34 -0500 Subject: [PATCH 1/2] bump(github.com/openshift/api): Pick up changes to UpdateHistory --- Gopkg.lock | 7 +- .../openshift/api/config/v1/register.go | 1 - .../openshift/api/config/v1/types.go | 17 +- .../api/config/v1/types_authentication.go | 38 +- .../openshift/api/config/v1/types_build.go | 34 +- .../api/config/v1/types_cluster_version.go | 58 +- .../openshift/api/config/v1/types_dns.go | 6 + .../api/config/v1/types_infrastructure.go | 24 +- .../openshift/api/config/v1/types_ingress.go | 5 +- .../openshift/api/config/v1/types_network.go | 46 +- .../openshift/api/config/v1/types_oauth.go | 541 ++++++++++++++- .../config/v1/types_swagger_doc_generated.go | 12 +- .../api/config/v1/zz_generated.deepcopy.go | 622 +++++++++++++++++- 13 files changed, 1349 insertions(+), 62 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 476511f319..8ef54696b8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -182,7 +182,7 @@ [[projects]] branch = "master" - digest = "1:bada18f8cc9cf347266c81ee2ab8956b7c20ff6bf798b3d848e92f63a5c57330" + digest = "1:97f41aee849f8b913f2da5983e44be080c904161a3331701b6e1e112424c9db0" name = "github.com/openshift/api" packages = [ "config/v1", @@ -192,7 +192,7 @@ "security/v1", ] pruneopts = "NUT" - revision = "8dce450af78098c96d68dc2a0c2fb8e917993fb4" + revision = "aab033bae2a129607f4fb277c3777b2eabb08601" [[projects]] branch = "master" @@ -213,7 +213,7 @@ "security/clientset/versioned/typed/security/v1", ] pruneopts = "NUT" - revision = "960f72aa32a8e9b4dd769b90ff1cb5bd4c898eec" + revision = "2f38d042c79762d5bf2ba3c795d4ad72101289e9" [[projects]] branch = "master" @@ -754,6 +754,7 @@ "k8s.io/apimachinery/pkg/util/runtime", "k8s.io/apimachinery/pkg/util/sets", "k8s.io/apimachinery/pkg/util/strategicpatch", + "k8s.io/apimachinery/pkg/util/validation/field", "k8s.io/apimachinery/pkg/util/wait", "k8s.io/apimachinery/pkg/util/yaml", "k8s.io/apimachinery/pkg/watch", diff --git a/vendor/github.com/openshift/api/config/v1/register.go b/vendor/github.com/openshift/api/config/v1/register.go index 0a2ad39357..eed769098d 100644 --- a/vendor/github.com/openshift/api/config/v1/register.go +++ b/vendor/github.com/openshift/api/config/v1/register.go @@ -42,7 +42,6 @@ func addKnownTypes(scheme *runtime.Scheme) error { &ConsoleList{}, &DNS{}, &DNSList{}, - &GenericControllerConfig{}, &IdentityProvider{}, &IdentityProviderList{}, &Image{}, diff --git a/vendor/github.com/openshift/api/config/v1/types.go b/vendor/github.com/openshift/api/config/v1/types.go index 370c265c08..7445c4fff8 100644 --- a/vendor/github.com/openshift/api/config/v1/types.go +++ b/vendor/github.com/openshift/api/config/v1/types.go @@ -5,14 +5,21 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// ConfigMapReference references the location of a configmap. +// ConfigMapReference references a configmap in the openshift-config namespace. type ConfigMapReference struct { - Namespace string `json:"namespace"` - Name string `json:"name"` + Name string `json:"name"` // Key allows pointing to a specific key/value inside of the configmap. This is useful for logical file references. Key string `json:"filename,omitempty"` } +// LocalSecretReference references a secret within the local namespace +type LocalSecretReference struct { + // Name of the secret in the local namespace + Name string `json:"name"` + // Key selects a specific key within the local secret. Must be a valid secret key. + Key string `json:"key,omitempty"` +} + // HTTPServingInfo holds configuration for serving HTTP type HTTPServingInfo struct { // ServingInfo is the HTTP serving information @@ -245,12 +252,8 @@ type ClientConnectionOverrides struct { Burst int32 `json:"burst"` } -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - // GenericControllerConfig provides information to configure a controller type GenericControllerConfig struct { - metav1.TypeMeta `json:",inline"` - // ServingInfo is the HTTP serving information for the controller's endpoints ServingInfo HTTPServingInfo `json:"servingInfo,omitempty"` diff --git a/vendor/github.com/openshift/api/config/v1/types_authentication.go b/vendor/github.com/openshift/api/config/v1/types_authentication.go index 281dca7acd..af181c34e7 100644 --- a/vendor/github.com/openshift/api/config/v1/types_authentication.go +++ b/vendor/github.com/openshift/api/config/v1/types_authentication.go @@ -7,7 +7,6 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Authentication holds cluster-wide information about Authentication. The canonical name is `cluster` -// TODO this object is an example of a possible grouping and is subject to change or removal type Authentication struct { metav1.TypeMeta `json:",inline"` // Standard object's metadata. @@ -20,13 +19,34 @@ type Authentication struct { } type AuthenticationSpec struct { - // webhook token auth config (ttl) - // external token address - // serviceAccountOAuthGrantMethod or remove/disallow it as an option + // oauthMetadata contains the discovery endpoint data for OAuth 2.0 + // Authorization Server Metadata for an external OAuth server. + // This discovery document can be viewed from its served location: + // oc get --raw '/.well-known/oauth-authorization-server' + // For further details, see the IETF Draft: + // https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2 + // If oauthMetadata.name is non-empty, this value has precedence + // over the observed value stored in status.oauthMetadata + // +optional + OAuthMetadata ConfigMapReference `json:"oauthMetadata"` + + // webhookTokenAuthenticators configures remote token reviewers. + // These remote authentication webhooks can be used to verify bearer tokens + // via the tokenreviews.authentication.k8s.io REST API. This is required to + // honor bearer tokens that are provisioned by an external authentication service. + WebhookTokenAuthenticators []WebhookTokenAuthenticator `json:"webhookTokenAuthenticators"` } type AuthenticationStatus struct { - // internal token address + // oauthMetadata contains the discovery endpoint data for OAuth 2.0 + // Authorization Server Metadata for an external OAuth server. + // This discovery document can be viewed from its served location: + // oc get --raw '/.well-known/oauth-authorization-server' + // For further details, see the IETF Draft: + // https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2 + // This contains the observed value based on cluster state. + // An explicitly set value in spec.oauthMetadata has precedence over this field. + OAuthMetadata ConfigMapReference `json:"oauthMetadata"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -37,3 +57,11 @@ type AuthenticationList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []Authentication `json:"items"` } + +// webhookTokenAuthenticator holds the necessary configuration options for a remote token authenticator +type WebhookTokenAuthenticator struct { + // kubeConfig contains kube config file data which describes how to access the remote webhook service. + // For further details, see: + // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication + KubeConfig LocalSecretReference `json:"kubeConfig"` +} diff --git a/vendor/github.com/openshift/api/config/v1/types_build.go b/vendor/github.com/openshift/api/config/v1/types_build.go index 480c1d321c..49d3d0fe68 100644 --- a/vendor/github.com/openshift/api/config/v1/types_build.go +++ b/vendor/github.com/openshift/api/config/v1/types_build.go @@ -32,17 +32,20 @@ type BuildSpec struct { } type BuildDefaults struct { - // GitHTTPProxy is the location of the HTTPProxy for Git source + // DefaultProxy contains the default proxy settings for all build operations, including image pull/push + // and source download. + // + // Values can be overrode by setting the `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables + // in the build config's strategy. // +optional - GitHTTPProxy string `json:"gitHTTPProxy,omitempty"` + DefaultProxy *ProxyConfig `json:"defaultProxy,omitempty"` - // GitHTTPSProxy is the location of the HTTPSProxy for Git source + // GitProxy contains the proxy settings for git operations only. If set, this will override + // any Proxy settings for all git commands, such as git clone. + // + // Values that are not set here will be inherited from DefaultProxy. // +optional - GitHTTPSProxy string `json:"gitHTTPSProxy,omitempty"` - - // GitNoProxy is the list of domains for which the proxy should not be used - // +optional - GitNoProxy string `json:"gitNoProxy,omitempty"` + GitProxy *ProxyConfig `json:"gitProxy,omitempty"` // Env is a set of default environment variables that will be applied to the // build if the specified variables do not exist on the build @@ -69,6 +72,21 @@ type ImageLabel struct { Value string `json:"value,omitempty"` } +// ProxyConfig defines what proxies to use for an operation +type ProxyConfig struct { + // HttpProxy is the URL of the proxy for HTTP requests + // +optional + HTTPProxy string `json:"httpProxy,omitempty"` + + // HttpsProxy is the URL of the proxy for HTTPS requests + // +optional + HTTPSProxy string `json:"httpsProxy,omitempty"` + + // NoProxy is the list of domains for which the proxy should not be used + // +optional + NoProxy string `json:"noProxy,omitempty"` +} + type BuildOverrides struct { // ImageLabels is a list of docker labels that are applied to the resulting image. // If user provided a label in their Build/BuildConfig with the same name as one in this diff --git a/vendor/github.com/openshift/api/config/v1/types_cluster_version.go b/vendor/github.com/openshift/api/config/v1/types_cluster_version.go index e012443ea2..a43b12a69a 100644 --- a/vendor/github.com/openshift/api/config/v1/types_cluster_version.go +++ b/vendor/github.com/openshift/api/config/v1/types_cluster_version.go @@ -73,11 +73,19 @@ type ClusterVersionSpec struct { // progress, or is failing. // +k8s:deepcopy-gen=true type ClusterVersionStatus struct { - // current is the version that the cluster will be reconciled to. This - // value may be empty during cluster startup, and then will be set whenever - // a new update is being applied. Use the conditions array to know whether - // the update is complete. - Current Update `json:"current"` + // desired is the version that the cluster is reconciling towards. + // If the cluster is not yet fully initialized desired will be set + // with the information available, which may be a payload or a tag. + Desired Update `json:"desired"` + + // history contains a list of the most recent versions applied to the cluster. + // This value may be empty during cluster startup, and then will be updated + // when a new update is being applied. The newest update is first in the + // list and it is ordered by recency. Updates in the history have state + // Completed if the rollout completed - if an update was failing or halfway + // applied the state will be Partial. Only a limited amount of update history + // is preserved. + History []UpdateHistory `json:"history"` // generation reports which version of the spec is being processed. // If this value is not equal to metadata.generation, then the @@ -106,6 +114,46 @@ type ClusterVersionStatus struct { AvailableUpdates []Update `json:"availableUpdates"` } +// UpdateState is a constant representing whether an update was successfully +// applied to the cluster or not. +type UpdateState string + +const ( + // CompletedUpdate indicates an update was successfully applied + // to the cluster (all resource updates were successful). + CompletedUpdate UpdateState = "Completed" + // PartialUpdate indicates an update was never completely applied + // or is currently being applied. + PartialUpdate UpdateState = "Partial" +) + +// UpdateHistory is a single attempted update to the cluster. +type UpdateHistory struct { + // state reflects whether the update was fully applied. The Partial state + // indicates the update is not fully applied, while the Completed state + // indicates the update was successfully rolled out at least once (all + // parts of the update successfully applied). + State UpdateState `json:"state"` + + // startedTime is the time at which the update was started. + StartedTime metav1.Time `json:"startedTime"` + // completionTime, if set, is when the update was fully applied. The update + // that is currently being applied will have a null completion time. + // Completion time will always be set for entries that are not the current + // update (usually to the started time of the next update). + CompletionTime *metav1.Time `json:"completionTime"` + + // version is a semantic versioning identifying the update version. If the + // requested payload does not define a version, or if a failure occurs + // retrieving the payload, this value may be empty. + // + // +optional + Version string `json:"version"` + // payload is a container image location that contains the update. This value + // is always populated. + Payload string `json:"payload"` +} + // ClusterID is string RFC4122 uuid. type ClusterID string diff --git a/vendor/github.com/openshift/api/config/v1/types_dns.go b/vendor/github.com/openshift/api/config/v1/types_dns.go index 44fa6e4d27..c371895471 100644 --- a/vendor/github.com/openshift/api/config/v1/types_dns.go +++ b/vendor/github.com/openshift/api/config/v1/types_dns.go @@ -20,6 +20,12 @@ type DNS struct { } type DNSSpec struct { + // baseDomain is the base domain of the cluster. All managed DNS records will + // be sub-domains of this base. + // + // For example, given the base domain `openshift.example.com`, an API server + // DNS record may be created for `cluster-api.openshift.example.com`. + BaseDomain string `json:"baseDomain"` } type DNSStatus struct { diff --git a/vendor/github.com/openshift/api/config/v1/types_infrastructure.go b/vendor/github.com/openshift/api/config/v1/types_infrastructure.go index 234e872c0b..42efc79db9 100644 --- a/vendor/github.com/openshift/api/config/v1/types_infrastructure.go +++ b/vendor/github.com/openshift/api/config/v1/types_infrastructure.go @@ -25,9 +25,31 @@ type InfrastructureSpec struct { } type InfrastructureStatus struct { - // type + // platform is the underlying infrastructure provider for the cluster. This + // value controls whether infrastructure automation such as service load + // balancers, dynamic volume provisioning, machine creation and deletion, and + // other integrations are enabled. If None, no infrastructure automation is + // enabled. + Platform PlatformType `json:"platform,omitempty"` } +// platformType is a specific supported infrastructure provider. +type PlatformType string + +const ( + // awsPlatform represents Amazon AWS. + AWSPlatform PlatformType = "AWS" + + // openStackPlatform represents OpenStack. + OpenStackPlatform PlatformType = "OpenStack" + + // libvirtPlatform represents libvirt. + LibvirtPlatform PlatformType = "Libvirt" + + // nonePlatform means there is no infrastructure provider. + NonePlatform PlatformType = "None" +) + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type InfrastructureList struct { diff --git a/vendor/github.com/openshift/api/config/v1/types_ingress.go b/vendor/github.com/openshift/api/config/v1/types_ingress.go index e8467a0906..d9b81988f5 100644 --- a/vendor/github.com/openshift/api/config/v1/types_ingress.go +++ b/vendor/github.com/openshift/api/config/v1/types_ingress.go @@ -20,7 +20,10 @@ type Ingress struct { } type IngressSpec struct { - // default suffix. It goes here or it gets removed from server + // domain is used to generate a default host name for a route when the + // route's host name is empty. The generated host name will follow this + // pattern: "..". + Domain string `json:"domain"` } type IngressStatus struct { diff --git a/vendor/github.com/openshift/api/config/v1/types_network.go b/vendor/github.com/openshift/api/config/v1/types_network.go index aaea1aab14..144ba15b53 100644 --- a/vendor/github.com/openshift/api/config/v1/types_network.go +++ b/vendor/github.com/openshift/api/config/v1/types_network.go @@ -13,20 +13,56 @@ type Network struct { // Standard object's metadata. metav1.ObjectMeta `json:"metadata,omitempty"` - // spec holds user settable values for configuration + // spec holds user settable values for configuration. Spec NetworkSpec `json:"spec"` // status holds observed values from the cluster. They may not be overridden. Status NetworkStatus `json:"status"` } +// NetworkSpec is the desired network configuration. +// As a general rule, this SHOULD NOT be read directly. Instead, you should +// consume the NetworkStatus, as it indicates the currently deployed configuration. +// Currently, none of these fields may be changed after installation. type NetworkSpec struct { - // serviceCIDR - // servicePortRange - // vxlanPort - // ClusterNetworks []ClusterNetworkEntry `json:"clusterNetworks"` + // IP address pool to use for pod IPs. + ClusterNetwork []ClusterNetworkEntry `json:"clusterNetwork"` + + // IP address pool for services. + // Currently, we only support a single entry here. + ServiceNetwork []string `json:"serviceNetwork"` + + // NetworkType is the plugin that is to be deployed (e.g. OpenShiftSDN). + // This should match a value that the cluster-network-operator understands, + // or else no networking will be installed. + // Currently supported values are: + // - OpenShiftSDN + NetworkType string `json:"networkType"` } +// NetworkStatus is the current network configuration. type NetworkStatus struct { + // IP address pool to use for pod IPs. + ClusterNetwork []ClusterNetworkEntry `json:"clusterNetwork"` + + // IP address pool for services. + // Currently, we only support a single entry here. + ServiceNetwork []string `json:"serviceNetwork"` + + // NetworkType is the plugin that is deployed (e.g. OpenShiftSDN). + NetworkType string `json:"networkType"` + + // ClusterNetworkMTU is the MTU for inter-pod networking. + ClusterNetworkMTU int `json:"clusterNetworkMTU"` +} + +// ClusterNetworkEntry is a contiguous block of IP addresses from which pod IPs +// are allocated. +type ClusterNetworkEntry struct { + // The complete block for pod IPs. + CIDR string `json:"cidr"` + + // The size (prefix) of block to allocate to each node. + HostPrefix uint32 `json:"hostPrefix"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/vendor/github.com/openshift/api/config/v1/types_oauth.go b/vendor/github.com/openshift/api/config/v1/types_oauth.go index d4402ed338..91cffacdc1 100644 --- a/vendor/github.com/openshift/api/config/v1/types_oauth.go +++ b/vendor/github.com/openshift/api/config/v1/types_oauth.go @@ -1,37 +1,550 @@ package v1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// OAuth Server and Identity Provider Config // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // OAuth holds cluster-wide information about OAuth. The canonical name is `cluster` -// TODO this object is an example of a possible grouping and is subject to change or removal type OAuth struct { - metav1.TypeMeta `json:",inline"` - // Standard object's metadata. - metav1.ObjectMeta `json:"metadata,omitempty"` - - // spec holds user settable values for configuration - Spec OAuthSpec `json:"spec"` - // status holds observed values from the cluster. They may not be overridden. - Status OAuthStatus `json:"status"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec OAuthSpec `json:"spec"` + Status OAuthStatus `json:"status,omitempty"` } +// OAuthSpec contains desired cluster auth configuration type OAuthSpec struct { - // options for configuring the embedded oauth server. - // possibly wellknown? + // identityProviders is an ordered list of ways for a user to identify themselves + IdentityProviders []OAuthIdentityProvider `json:"identityProviders"` + + // tokenConfig contains options for authorization and access tokens + TokenConfig TokenConfig `json:"tokenConfig"` + + // templates allow you to customize pages like the login page. + // +optional + Templates OAuthTemplates `json:"templates"` } +// OAuthStatus shows current known state of OAuth server in the cluster type OAuthStatus struct { + // TODO Fill in } -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// TokenConfig holds the necessary configuration options for authorization and access tokens +type TokenConfig struct { + // authorizeTokenMaxAgeSeconds defines the maximum age of authorize tokens + AuthorizeTokenMaxAgeSeconds int32 `json:"authorizeTokenMaxAgeSeconds"` + // accessTokenMaxAgeSeconds defines the maximum age of access tokens + AccessTokenMaxAgeSeconds int32 `json:"accessTokenMaxAgeSeconds"` + // accessTokenInactivityTimeoutSeconds defines the default token + // inactivity timeout for tokens granted by any client. + // The value represents the maximum amount of time that can occur between + // consecutive uses of the token. Tokens become invalid if they are not + // used within this temporal window. The user will need to acquire a new + // token to regain access once a token times out. + // Valid values are integer values: + // x < 0 Tokens time out is enabled but tokens never timeout unless configured per client (e.g. `-1`) + // x = 0 Tokens time out is disabled (default) + // x > 0 Tokens time out if there is no activity for x seconds + // The current minimum allowed value for X is 300 (5 minutes) + // +optional + AccessTokenInactivityTimeoutSeconds int32 `json:"accessTokenInactivityTimeoutSeconds,omitempty"` +} + +const ( + // LoginTemplateKey is the default key of the login template + LoginTemplateKey = "login.html" + // ProviderSelectionTemplateKey is the default key for the provider selection template + ProviderSelectionTemplateKey = "providers.html" + // ErrorsTemplateKey is the default key for the errors template + ErrorsTemplateKey = "errors.html" +) + +// OAuthTemplates allow for customization of pages like the login page +type OAuthTemplates struct { + // login is a reference to a secret that specifies a go template to use to render the login page. + // If a key is not specified, the key `login.html` is used to locate the template data. + // If unspecified, the default login page is used. + // +optional + Login LocalSecretReference `json:"login,omitemtpy"` + + // providerSelection is a reference to a secret that specifies a go template to use to render + // the provider selection page. + // If a key is not specified, the key `providers.html` is used to locate the template data. + // If unspecified, the default provider selection page is used. + // +optional + ProviderSelection LocalSecretReference `json:"providerSelection,omitempty"` + + // error is a reference to a secret that specifies a go template to use to render error pages + // during the authentication or grant flow. + // If a key is not specified, the key `errrors.html` is used to locate the template data. + // If unspecified, the default error page is used. + // +optional + Error LocalSecretReference `json:"error,omitempty"` +} + +// OAuthIdentityProvider provides identities for users authenticating using credentials +type OAuthIdentityProvider struct { + // name is used to qualify the identities returned by this provider. + // - It MUST be unique and not shared by any other identity provider used + // - It MUST be a vlid path segment: name cannot equal "." or ".." or contain "/" or "%" + // Ref: https://godoc.org/k8s.io/apimachinery/pkg/api/validation/path#ValidatePathSegmentName + Name string `json:"name"` + + // challenge indicates whether to issue WWW-Authenticate challenges for this provider + UseAsChallenger bool `json:"challenge"` + // login indicates whether to use this identity provider for unauthenticated browsers to login against + UseAsLogin bool `json:"login"` + + // mappingMethod determines how identities from this provider are mapped to users + // Defaults to "claim" + // +optional + MappingMethod MappingMethodType `json:"mappingMethod"` + + // grantMethod: allow, deny, prompt + // This method will be used only if the specific OAuth client doesn't provide a strategy + // of their own. Valid grant handling methods are: + // - auto: always approves grant requests, useful for trusted clients + // - prompt: prompts the end user for approval of grant requests, useful for third-party clients + // - deny: always denies grant requests, useful for black-listed clients + // Defaults to "prompt" if not set. + // +optional + GrantMethod GrantHandlerType `json:"grantMethod"` + + // IdentityProvidersConfig + ProviderConfig IdentityProviderConfig `json:",inline"` +} + +// MappingMethodType specifies how new identities should be mapped to users when they log in +type MappingMethodType string + +const ( + // MappingMethodClaim provisions a user with the identity’s preferred user name. Fails if a user + // with that user name is already mapped to another identity. + // Default. + MappingMethodClaim MappingMethodType = "claim" + + // MappingMethodLookup looks up existing users already mapped to an identity but does not + // automatically provision users or identities. Requires identities and users be set up + // manually or using an external process. + MappingMethodLookup MappingMethodType = "lookup" + + // MappingMethodAdd provisions a user with the identity’s preferred user name. If a user with + // that user name already exists, the identity is mapped to the existing user, adding to any + // existing identity mappings for the user. + MappingMethodAdd MappingMethodType = "add" + + // MappingMethodGenerate provisions a user with the identity’s preferred user name. If a user + // with the preferred user name is already mapped to an existing identity, a unique user name is + // generated, e.g. myuser2. This method should not be used in combination with external + // processes that require exact matches between openshift user names and the idp user name + // such as LDAP group sync. + MappingMethodGenerate MappingMethodType = "generate" +) + +// GrantHandlerType are the valid strategies for handling grant requests +type GrantHandlerType string + +const ( + // GrantHandlerAuto auto-approves client authorization grant requests + GrantHandlerAuto GrantHandlerType = "auto" + // GrantHandlerPrompt prompts the user to approve new client authorization grant requests + GrantHandlerPrompt GrantHandlerType = "prompt" + // GrantHandlerDeny auto-denies client authorization grant requests + GrantHandlerDeny GrantHandlerType = "deny" +) + +type IdentityProviderType string + +const ( + // IdentityProviderTypeBasicAuth provides identities for users authenticating with HTTP Basic Auth + IdentityProviderTypeBasicAuth IdentityProviderType = "BasicAuth" + + // IdentityProviderTypeAllowAll provides identities for all users authenticating using non-empty passwords + IdentityProviderTypeAllowAll IdentityProviderType = "AllowAll" + + // IdentityProviderTypeDenyAll provides no identities for users + IdentityProviderTypeDenyAll IdentityProviderType = "DenyAll" + + // IdentityProviderTypeHTPasswd provides identities from an HTPasswd file + IdentityProviderTypeHTPasswd IdentityProviderType = "HTPasswd" + + // IdentityProviderTypeLDAP provides identities for users authenticating using LDAP credentials + IdentityProviderTypeLDAP IdentityProviderType = "LDAP" + + // IdentityProviderTypeKeystone provides identitities for users authenticating using keystone password credentials + IdentityProviderTypeKeystone IdentityProviderType = "Keystone" + + // IdentityProviderTypeRequestHeader provides identities for users authenticating using request header credentials + IdentityProviderTypeRequestHeader IdentityProviderType = "RequestHeader" + + // IdentityProviderTypeGitHub provides identities for users authenticating using GitHub credentials + IdentityProviderTypeGitHub IdentityProviderType = "GitHub" + + // IdentityProviderTypeGitLab provides identities for users authenticating using GitLab credentials + IdentityProviderTypeGitLab IdentityProviderType = "GitLab" + + // IdentityProviderTypeGoogle provides identities for users authenticating using Google credentials + IdentityProviderTypeGoogle IdentityProviderType = "Google" + + // IdentityProviderTypeOpenID provides identities for users authenticating using OpenID credentials + IdentityProviderTypeOpenID IdentityProviderType = "OpenID" +) + +// IdentityProviderConfig contains configuration for using a specific identity provider +type IdentityProviderConfig struct { + // type identifies the identity provider type for this entry. + Type IdentityProviderType `json:"type"` + + // Provider-specific configuration + // The json tag MUST match the `Type` specified above, case-insensitively + // e.g. For `Type: "LDAP"`, the `LDAPPasswordIdentityProvider` configuration should be provided + + // basicAuth contains configuration options for the BasicAuth IdP + // +optional + BasicAuth *BasicAuthPasswordIdentityProvider `json:"basicAuth,omitempty"` + + // allowAll enables the AllowAllIdentityProvider which provides identities for users + // authenticating using non-empty passwords. + // Defaults to `false`, i.e. allowAll set to off + // +optional + AllowAll bool `json:"allowAll,omitempty"` + + // denyAll enables the DenyAllPasswordIdentityProvider which provides no identities for users + // Defaults to `false`, ie. denyAll set to off + // +optional + DenyAll bool `json:"denyAll,omitempty"` + + // htpasswd enables user authentication using an HTPasswd file to validate credentials + // +optional + HTPasswd *HTPasswdPasswordIdentityProvider `json:"htpasswd,omitempty"` + + // ldap enables user authentication using LDAP credentials + // +optional + LDAP *LDAPPasswordIdentityProvider `json:"ldap,omitempty"` + + // keystone enables user authentication using keystone password credentials + // +optional + Keystone *KeystonePasswordIdentityProvider `json:"keystone,omitempty"` + + // requestHeader enables user authentication using request header credentials + RequestHeader *RequestHeaderIdentityProvider `json:"requestHeader,omitempty"` + + // github enables user authentication using GitHub credentials + // +optional + GitHub *GitHubIdentityProvider `json:"github,omitempty"` + + // gitlab enables user authentication using GitLab credentials + // +optional + GitLab *GitLabIdentityProvider `json:"gitlab,omitempty"` + + // google enables user authentication using Google credentials + // +optional + Google *GoogleIdentityProvider `json:"google,omitempty"` + + // openID enables user authentication using OpenID credentials + // +optional + OpenID *OpenIDIdentityProvider `json:"openID,omitempty"` +} + +// BasicAuthPasswordIdentityProvider provides identities for users authenticating using HTTP basic auth credentials +type BasicAuthPasswordIdentityProvider struct { + // OAuthRemoteConnectionInfo contains information about how to connect to the external basic auth server + OAuthRemoteConnectionInfo `json:",inline"` +} + +// RemoteConnectionInfo holds information necessary for establishing a remote connection +type OAuthRemoteConnectionInfo struct { + // url is the remote URL to connect to + URL string `json:"url"` + // ca is a reference to a ConfigMap containing the CA for verifying TLS connections + CA ConfigMapReference `json:"ca"` + + // tlsClientCert references a secret containing the TLS client certificate to present when + // connecting to the server. + // Looks under the key "tls.cert" for the data unless a lookup key is specified in the secret ref + TLSClientCert LocalSecretReference `json:"tlsClientCert"` + + // tlsClientKey references a secret containing the TLS private key for the client certificate + // Looks under the key "tls.key" for the data unless a lookup key is specified in the secret ref + TLSClientKey LocalSecretReference `json:"tlsClientKey"` +} + +// HTPasswdDataKey is the default key for the htpasswd file data in a secret +const HTPasswdDataKey = "htpasswd" + +// HTPasswdPasswordIdentityProvider provides identities for users authenticating using htpasswd credentials +type HTPasswdPasswordIdentityProvider struct { + // fileData is a reference to a secret containing the data to use as the htpasswd file + // Looks under the key `htpasswd` unless a lookup key is specified in the secret ref + FileData LocalSecretReference `json:"fileData"` +} + +const ( + // BindPasswordKey is default the key for the LDAP bind password in a secret + BindPasswordKey = "bindPassword" + // ClientSecretKey is the key for the oauth client secret data in a secret + ClientSecretKey = "clientSecret" +) + +// LDAPPasswordIdentityProvider provides identities for users authenticating using LDAP credentials +type LDAPPasswordIdentityProvider struct { + // url is an RFC 2255 URL which specifies the LDAP search parameters to use. + // The syntax of the URL is: + // ldap://host:port/basedn?attribute?scope?filter + URL string `json:"url"` + + // bindDN is an optional DN to bind with during the search phase. + // +optional + BindDN string `json:"bindDN"` + + // bindPassword is a reference to the secret containing an optional password to bind + // with during the search phase. + // Looks under the key `bindPassword` unless a lookup key is specified in the secret ref + // +optional + BindPassword LocalSecretReference `json:"bindPassword"` + + // insecure, if true, indicates the connection should not use TLS + // WARNING: Should not be set to `true` with the URL scheme "ldaps://" as "ldaps://" URLs always + // attempt to connect using TLS, even when `insecure` is set to `true` + // When `true`, "ldap://" URLS connect insecurely. When `false`, "ldap://" URLs are upgraded to + // a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830. + Insecure bool `json:"insecure"` + + // ca is a reference to a ConfigMap containing an optional trusted certificate authority bundle + // to use when making requests to the server. + // If empty, the default system roots are used. + // +optional + CA ConfigMapReference `json:"ca"` + + // attributes maps LDAP attributes to identities + Attributes LDAPAttributeMapping `json:"attributes"` +} + +// LDAPAttributeMapping maps LDAP attributes to OpenShift identity fields +type LDAPAttributeMapping struct { + // id is the list of attributes whose values should be used as the user ID. Required. + // First non-empty attribute is used. At least one attribute is required. If none of the listed + // attribute have a value, authentication fails. + // LDAP standard identity attribute is "dn" + ID []string `json:"id"` + // preferredUsername is the list of attributes whose values should be used as the preferred username. + // LDAP standard login attribute is "uid" + // +optional + PreferredUsername []string `json:"preferredUsername"` + // name is the list of attributes whose values should be used as the display name. Optional. + // If unspecified, no display name is set for the identity + // LDAP standard display name attribute is "cn" + // +optional + Name []string `json:"name"` + // email is the list of attributes whose values should be used as the email address. Optional. + // If unspecified, no email is set for the identity + // +optional + Email []string `json:"email"` +} + +// KeystonePasswordIdentityProvider provides identities for users authenticating using keystone password credentials +type KeystonePasswordIdentityProvider struct { + // OAuthRemoteConnectionInfo contains information about how to connect to the keystone server + OAuthRemoteConnectionInfo `json:",inline"` + // domainName is required for keystone v3 + DomainName string `json:"domainName"` + // useKeystoneIdentity flag indicates that user should be authenticated by username, not keystone ID + // DEPRECATED - only use this option for legacy systems to ensure backwards compatibiity + // +optional + LegacyLookupByUsername bool `json:"useKeystoneIdentity"` +} + +// RequestHeaderIdentityProvider provides identities for users authenticating using request header credentials +type RequestHeaderIdentityProvider struct { + // loginURL is a URL to redirect unauthenticated /authorize requests to + // Unauthenticated requests from OAuth clients which expect interactive logins will be redirected here + // ${url} is replaced with the current URL, escaped to be safe in a query parameter + // https://www.example.com/sso-login?then=${url} + // ${query} is replaced with the current query string + // https://www.example.com/auth-proxy/oauth/authorize?${query} + // Required when UseAsLogin is set to true. + LoginURL string `json:"loginURL"` + + // challengeURL is a URL to redirect unauthenticated /authorize requests to + // Unauthenticated requests from OAuth clients which expect WWW-Authenticate challenges will be + // redirected here. + // ${url} is replaced with the current URL, escaped to be safe in a query parameter + // https://www.example.com/sso-login?then=${url} + // ${query} is replaced with the current query string + // https://www.example.com/auth-proxy/oauth/authorize?${query} + // Required when UseAsChallenger is set to true. + ChallengeURL string `json:"challengeURL"` + + // clientCA is a reference to a configmap with the trusted signer certs. If empty, no request + // verification is done, and any direct request to the OAuth server can impersonate any identity + // from this provider, merely by setting a request header. + // +optional + ClientCA ConfigMapReference `json:"ca"` + + // clientCommonNames is an optional list of common names to require a match from. If empty, any + // client certificate validated against the clientCA bundle is considered authoritative. + // +optional + ClientCommonNames []string `json:"clientCommonNames"` + + // headers is the set of headers to check for identity information + Headers []string `json:"headers"` + + // preferredUsernameHeaders is the set of headers to check for the preferred username + PreferredUsernameHeaders []string `json:"preferredUsernameHeaders"` + + // nameHeaders is the set of headers to check for the display name + NameHeaders []string `json:"nameHeaders"` + + // emailHeaders is the set of headers to check for the email address + EmailHeaders []string `json:"emailHeaders"` +} + +// GitHubIdentityProvider provides identities for users authenticating using GitHub credentials +type GitHubIdentityProvider struct { + // clientID is the oauth client ID + ClientID string `json:"clientID"` + + // clientSecret is is a reference to the secret containing the oauth client secret + // The secret referenced must contain a key named `clientSecret` containing the secret data. + ClientSecret LocalSecretReference `json:"clientSecret"` + + // organizations optionally restricts which organizations are allowed to log in + // +optional + Organizations []string `json:"organizations"` + // teams optionally restricts which teams are allowed to log in. Format is /. + // +optional + Teams []string `json:"teams"` + + // hostname is the optional domain (e.g. "mycompany.com") for use with a hosted instance of + // GitHub Enterprise. + // It must match the GitHub Enterprise settings value configured at /setup/settings#hostname. + // +optional + Hostname string `json:"hostname"` + + // ca is a reference to a ConfigMap containing an optional trusted certificate authority bundle + // to use when making requests to the server. + // If empty, the default system roots are used. + // This can only be configured when hostname is set to a non-empty value. + // +optional + CA ConfigMapReference `json:"ca"` +} + +// GitLabIdentityProvider provides identities for users authenticating using GitLab credentials +type GitLabIdentityProvider struct { + // ca is a reference to a ConfigMap containing an optional trusted certificate authority bundle + // to use when making requests to the server. + // If empty, the default system roots are used. + // +optional + CA ConfigMapReference `json:"ca"` + + // url is the oauth server base URL + URL string `json:"url"` + + // clientID is the oauth client ID + ClientID string `json:"clientID"` + + // clientSecret is is a reference to the secret containing the oauth client secret + // The secret referenced must contain a key named `clientSecret` containing the secret data. + ClientSecret LocalSecretReference `json:"clientSecret"` + + // legacy determines that OAuth2 should be used, not OIDC + // +optional + LegacyOAuth2 bool `json:"legacy,omitempty"` +} + +// GoogleIdentityProvider provides identities for users authenticating using Google credentials +type GoogleIdentityProvider struct { + // clientID is the oauth client ID + ClientID string `json:"clientID"` + + // clientSecret is is a reference to the secret containing the oauth client secret + // The secret referenced must contain a key named `clientSecret` containing the secret data. + ClientSecret LocalSecretReference `json:"clientSecret"` + + // hostedDomain is the optional Google App domain (e.g. "mycompany.com") to restrict logins to + // +optional + HostedDomain string `json:"hostedDomain"` +} + +// OpenIDIdentityProvider provides identities for users authenticating using OpenID credentials +type OpenIDIdentityProvider struct { + // ca is a reference to a ConfigMap containing an optional trusted certificate authority bundle + // to use when making requests to the server. + // If empty, the default system roots are used. + // +optional + CA ConfigMapReference `json:"ca"` + + // clientID is the oauth client ID + ClientID string `json:"clientID"` + + // clientSecret is is a reference to the secret containing the oauth client secret + // The secret referenced must contain a key named `clientSecret` containing the secret data. + ClientSecret LocalSecretReference `json:"clientSecret"` + + // extraScopes are any scopes to request in addition to the standard "openid" scope. + // +optional + ExtraScopes []string `json:"extraScopes"` + + // extraAuthorizeParameters are any custom parameters to add to the authorize request. + // +optional + ExtraAuthorizeParameters map[string]string `json:"extraAuthorizeParameters"` + + // urls to use to authenticate + URLs OpenIDURLs `json:"urls"` + + // claims mappings + Claims OpenIDClaims `json:"claims"` +} + +// OpenIDURLs are URLs to use when authenticating with an OpenID identity provider +type OpenIDURLs struct { + // authorize is the oauth authorization URL + Authorize string `json:"authorize"` + // token is the oauth token granting URL + Token string `json:"token"` + // userInfo is the optional userinfo URL. + // If present, a granted access_token is used to request claims + // If empty, a granted id_token is parsed for claims + // +optional + UserInfo string `json:"userInfo"` +} + +// UserIDClaim is used in the `ID` field for an `OpenIDClaim` +// Per http://openid.net/specs/openid-connect-core-1_0.html#ClaimStability +// "The sub (subject) and iss (issuer) Claims, used together, are the only Claims that an RP can +// rely upon as a stable identifier for the End-User, since the sub Claim MUST be locally unique +// and never reassigned within the Issuer for a particular End-User, as described in Section 2. +// Therefore, the only guaranteed unique identifier for a given End-User is the combination of the +// iss Claim and the sub Claim." +const UserIDClaim = "sub" + +// OpenIDClaims contains a list of OpenID claims to use when authenticating with an OpenID identity provider +type OpenIDClaims struct { + // preferredUsername is the list of claims whose values should be used as the preferred username. + // If unspecified, the preferred username is determined from the value of the id claim + // +optional + PreferredUsername []string `json:"preferredUsername"` + // name is the list of claims whose values should be used as the display name. Optional. + // If unspecified, no display name is set for the identity + // +optional + Name []string `json:"name"` + // email is the list of claims whose values should be used as the email address. Optional. + // If unspecified, no email is set for the identity + // +optional + Email []string `json:"email"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OAuthList struct { metav1.TypeMeta `json:",inline"` - // Standard object's metadata. metav1.ListMeta `json:"metadata,omitempty"` Items []OAuth `json:"items"` } diff --git a/vendor/github.com/openshift/api/config/v1/types_swagger_doc_generated.go b/vendor/github.com/openshift/api/config/v1/types_swagger_doc_generated.go index 1fd8c16e8a..55b534c2e1 100644 --- a/vendor/github.com/openshift/api/config/v1/types_swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/config/v1/types_swagger_doc_generated.go @@ -61,7 +61,7 @@ func (ClientConnectionOverrides) SwaggerDoc() map[string]string { } var map_ConfigMapReference = map[string]string{ - "": "ConfigMapReference references the location of a configmap.", + "": "ConfigMapReference references a configmap in the openshift-config namespace.", "filename": "Key allows pointing to a specific key/value inside of the configmap. This is useful for logical file references.", } @@ -162,6 +162,16 @@ func (LeaderElection) SwaggerDoc() map[string]string { return map_LeaderElection } +var map_LocalSecretReference = map[string]string{ + "": "LocalSecretReference references a secret within the local namespace", + "name": "Name of the secret in the local namespace", + "key": "Key selects a specific key within the local secret. Must be a valid secret key.", +} + +func (LocalSecretReference) SwaggerDoc() map[string]string { + return map_LocalSecretReference +} + var map_NamedCertificate = map[string]string{ "": "NamedCertificate specifies a certificate/key, and the names it should be served for", "names": "Names is a list of DNS names this certificate should be used to secure A name can be a normal DNS name, or can contain leading wildcard segments.", diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go index 2e376017cf..e0fed9683e 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go @@ -48,7 +48,7 @@ func (in *Authentication) DeepCopyInto(out *Authentication) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -107,6 +107,12 @@ func (in *AuthenticationList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationSpec) DeepCopyInto(out *AuthenticationSpec) { *out = *in + out.OAuthMetadata = in.OAuthMetadata + if in.WebhookTokenAuthenticators != nil { + in, out := &in.WebhookTokenAuthenticators, &out.WebhookTokenAuthenticators + *out = make([]WebhookTokenAuthenticator, len(*in)) + copy(*out, *in) + } return } @@ -123,6 +129,7 @@ func (in *AuthenticationSpec) DeepCopy() *AuthenticationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationStatus) DeepCopyInto(out *AuthenticationStatus) { *out = *in + out.OAuthMetadata = in.OAuthMetadata return } @@ -136,6 +143,23 @@ func (in *AuthenticationStatus) DeepCopy() *AuthenticationStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthPasswordIdentityProvider) DeepCopyInto(out *BasicAuthPasswordIdentityProvider) { + *out = *in + out.OAuthRemoteConnectionInfo = in.OAuthRemoteConnectionInfo + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthPasswordIdentityProvider. +func (in *BasicAuthPasswordIdentityProvider) DeepCopy() *BasicAuthPasswordIdentityProvider { + if in == nil { + return nil + } + out := new(BasicAuthPasswordIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Build) DeepCopyInto(out *Build) { *out = *in @@ -166,6 +190,24 @@ func (in *Build) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BuildDefaults) DeepCopyInto(out *BuildDefaults) { *out = *in + if in.DefaultProxy != nil { + in, out := &in.DefaultProxy, &out.DefaultProxy + if *in == nil { + *out = nil + } else { + *out = new(ProxyConfig) + **out = **in + } + } + if in.GitProxy != nil { + in, out := &in.GitProxy, &out.GitProxy + if *in == nil { + *out = nil + } else { + *out = new(ProxyConfig) + **out = **in + } + } if in.Env != nil { in, out := &in.Env, &out.Env *out = make([]core_v1.EnvVar, len(*in)) @@ -305,6 +347,22 @@ func (in *ClientConnectionOverrides) DeepCopy() *ClientConnectionOverrides { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterNetworkEntry) DeepCopyInto(out *ClusterNetworkEntry) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterNetworkEntry. +func (in *ClusterNetworkEntry) DeepCopy() *ClusterNetworkEntry { + if in == nil { + return nil + } + out := new(ClusterNetworkEntry) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterOperator) DeepCopyInto(out *ClusterOperator) { *out = *in @@ -517,7 +575,14 @@ func (in *ClusterVersionSpec) DeepCopy() *ClusterVersionSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterVersionStatus) DeepCopyInto(out *ClusterVersionStatus) { *out = *in - out.Current = in.Current + out.Desired = in.Desired + if in.History != nil { + in, out := &in.History, &out.History + *out = make([]UpdateHistory, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]ClusterOperatorStatusCondition, len(*in)) @@ -869,7 +934,6 @@ func (in *GenericAPIServerConfig) DeepCopy() *GenericAPIServerConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GenericControllerConfig) DeepCopyInto(out *GenericControllerConfig) { *out = *in - out.TypeMeta = in.TypeMeta in.ServingInfo.DeepCopyInto(&out.ServingInfo) out.LeaderElection = in.LeaderElection out.Authentication = in.Authentication @@ -887,12 +951,84 @@ func (in *GenericControllerConfig) DeepCopy() *GenericControllerConfig { return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *GenericControllerConfig) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.ClientSecret = in.ClientSecret + if in.Organizations != nil { + in, out := &in.Organizations, &out.Organizations + *out = make([]string, len(*in)) + copy(*out, *in) } - return nil + if in.Teams != nil { + in, out := &in.Teams, &out.Teams + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.CA = in.CA + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitLabIdentityProvider) DeepCopyInto(out *GitLabIdentityProvider) { + *out = *in + out.CA = in.CA + out.ClientSecret = in.ClientSecret + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitLabIdentityProvider. +func (in *GitLabIdentityProvider) DeepCopy() *GitLabIdentityProvider { + if in == nil { + return nil + } + out := new(GitLabIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GoogleIdentityProvider) DeepCopyInto(out *GoogleIdentityProvider) { + *out = *in + out.ClientSecret = in.ClientSecret + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GoogleIdentityProvider. +func (in *GoogleIdentityProvider) DeepCopy() *GoogleIdentityProvider { + if in == nil { + return nil + } + out := new(GoogleIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTPasswdPasswordIdentityProvider) DeepCopyInto(out *HTPasswdPasswordIdentityProvider) { + *out = *in + out.FileData = in.FileData + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTPasswdPasswordIdentityProvider. +func (in *HTPasswdPasswordIdentityProvider) DeepCopy() *HTPasswdPasswordIdentityProvider { + if in == nil { + return nil + } + out := new(HTPasswdPasswordIdentityProvider) + in.DeepCopyInto(out) + return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -940,6 +1076,103 @@ func (in *IdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IdentityProviderConfig) DeepCopyInto(out *IdentityProviderConfig) { + *out = *in + if in.BasicAuth != nil { + in, out := &in.BasicAuth, &out.BasicAuth + if *in == nil { + *out = nil + } else { + *out = new(BasicAuthPasswordIdentityProvider) + **out = **in + } + } + if in.HTPasswd != nil { + in, out := &in.HTPasswd, &out.HTPasswd + if *in == nil { + *out = nil + } else { + *out = new(HTPasswdPasswordIdentityProvider) + **out = **in + } + } + if in.LDAP != nil { + in, out := &in.LDAP, &out.LDAP + if *in == nil { + *out = nil + } else { + *out = new(LDAPPasswordIdentityProvider) + (*in).DeepCopyInto(*out) + } + } + if in.Keystone != nil { + in, out := &in.Keystone, &out.Keystone + if *in == nil { + *out = nil + } else { + *out = new(KeystonePasswordIdentityProvider) + **out = **in + } + } + if in.RequestHeader != nil { + in, out := &in.RequestHeader, &out.RequestHeader + if *in == nil { + *out = nil + } else { + *out = new(RequestHeaderIdentityProvider) + (*in).DeepCopyInto(*out) + } + } + if in.GitHub != nil { + in, out := &in.GitHub, &out.GitHub + if *in == nil { + *out = nil + } else { + *out = new(GitHubIdentityProvider) + (*in).DeepCopyInto(*out) + } + } + if in.GitLab != nil { + in, out := &in.GitLab, &out.GitLab + if *in == nil { + *out = nil + } else { + *out = new(GitLabIdentityProvider) + **out = **in + } + } + if in.Google != nil { + in, out := &in.Google, &out.Google + if *in == nil { + *out = nil + } else { + *out = new(GoogleIdentityProvider) + **out = **in + } + } + if in.OpenID != nil { + in, out := &in.OpenID, &out.OpenID + if *in == nil { + *out = nil + } else { + *out = new(OpenIDIdentityProvider) + (*in).DeepCopyInto(*out) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentityProviderConfig. +func (in *IdentityProviderConfig) DeepCopy() *IdentityProviderConfig { + if in == nil { + return nil + } + out := new(IdentityProviderConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IdentityProviderList) DeepCopyInto(out *IdentityProviderList) { *out = *in @@ -1316,6 +1549,23 @@ func (in *IngressStatus) DeepCopy() *IngressStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeystonePasswordIdentityProvider) DeepCopyInto(out *KeystonePasswordIdentityProvider) { + *out = *in + out.OAuthRemoteConnectionInfo = in.OAuthRemoteConnectionInfo + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeystonePasswordIdentityProvider. +func (in *KeystonePasswordIdentityProvider) DeepCopy() *KeystonePasswordIdentityProvider { + if in == nil { + return nil + } + out := new(KeystonePasswordIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubeClientConfig) DeepCopyInto(out *KubeClientConfig) { *out = *in @@ -1333,6 +1583,61 @@ func (in *KubeClientConfig) DeepCopy() *KubeClientConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPAttributeMapping) DeepCopyInto(out *LDAPAttributeMapping) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PreferredUsername != nil { + in, out := &in.PreferredUsername, &out.PreferredUsername + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Email != nil { + in, out := &in.Email, &out.Email + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPAttributeMapping. +func (in *LDAPAttributeMapping) DeepCopy() *LDAPAttributeMapping { + if in == nil { + return nil + } + out := new(LDAPAttributeMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPPasswordIdentityProvider) DeepCopyInto(out *LDAPPasswordIdentityProvider) { + *out = *in + out.BindPassword = in.BindPassword + out.CA = in.CA + in.Attributes.DeepCopyInto(&out.Attributes) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPPasswordIdentityProvider. +func (in *LDAPPasswordIdentityProvider) DeepCopy() *LDAPPasswordIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPPasswordIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LeaderElection) DeepCopyInto(out *LeaderElection) { *out = *in @@ -1352,6 +1657,22 @@ func (in *LeaderElection) DeepCopy() *LeaderElection { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference. +func (in *LocalSecretReference) DeepCopy() *LocalSecretReference { + if in == nil { + return nil + } + out := new(LocalSecretReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamedCertificate) DeepCopyInto(out *NamedCertificate) { *out = *in @@ -1379,8 +1700,8 @@ func (in *Network) DeepCopyInto(out *Network) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } @@ -1438,6 +1759,16 @@ func (in *NetworkList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) { *out = *in + if in.ClusterNetwork != nil { + in, out := &in.ClusterNetwork, &out.ClusterNetwork + *out = make([]ClusterNetworkEntry, len(*in)) + copy(*out, *in) + } + if in.ServiceNetwork != nil { + in, out := &in.ServiceNetwork, &out.ServiceNetwork + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -1454,6 +1785,16 @@ func (in *NetworkSpec) DeepCopy() *NetworkSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkStatus) DeepCopyInto(out *NetworkStatus) { *out = *in + if in.ClusterNetwork != nil { + in, out := &in.ClusterNetwork, &out.ClusterNetwork + *out = make([]ClusterNetworkEntry, len(*in)) + copy(*out, *in) + } + if in.ServiceNetwork != nil { + in, out := &in.ServiceNetwork, &out.ServiceNetwork + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -1472,7 +1813,7 @@ func (in *OAuth) DeepCopyInto(out *OAuth) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -1495,6 +1836,23 @@ func (in *OAuth) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OAuthIdentityProvider) DeepCopyInto(out *OAuthIdentityProvider) { + *out = *in + in.ProviderConfig.DeepCopyInto(&out.ProviderConfig) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuthIdentityProvider. +func (in *OAuthIdentityProvider) DeepCopy() *OAuthIdentityProvider { + if in == nil { + return nil + } + out := new(OAuthIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OAuthList) DeepCopyInto(out *OAuthList) { *out = *in @@ -1528,9 +1886,37 @@ func (in *OAuthList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OAuthRemoteConnectionInfo) DeepCopyInto(out *OAuthRemoteConnectionInfo) { + *out = *in + out.CA = in.CA + out.TLSClientCert = in.TLSClientCert + out.TLSClientKey = in.TLSClientKey + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuthRemoteConnectionInfo. +func (in *OAuthRemoteConnectionInfo) DeepCopy() *OAuthRemoteConnectionInfo { + if in == nil { + return nil + } + out := new(OAuthRemoteConnectionInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OAuthSpec) DeepCopyInto(out *OAuthSpec) { *out = *in + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]OAuthIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.TokenConfig = in.TokenConfig + out.Templates = in.Templates return } @@ -1560,6 +1946,104 @@ func (in *OAuthStatus) DeepCopy() *OAuthStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OAuthTemplates) DeepCopyInto(out *OAuthTemplates) { + *out = *in + out.Login = in.Login + out.ProviderSelection = in.ProviderSelection + out.Error = in.Error + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuthTemplates. +func (in *OAuthTemplates) DeepCopy() *OAuthTemplates { + if in == nil { + return nil + } + out := new(OAuthTemplates) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenIDClaims) DeepCopyInto(out *OpenIDClaims) { + *out = *in + if in.PreferredUsername != nil { + in, out := &in.PreferredUsername, &out.PreferredUsername + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Email != nil { + in, out := &in.Email, &out.Email + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenIDClaims. +func (in *OpenIDClaims) DeepCopy() *OpenIDClaims { + if in == nil { + return nil + } + out := new(OpenIDClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenIDIdentityProvider) DeepCopyInto(out *OpenIDIdentityProvider) { + *out = *in + out.CA = in.CA + out.ClientSecret = in.ClientSecret + if in.ExtraScopes != nil { + in, out := &in.ExtraScopes, &out.ExtraScopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExtraAuthorizeParameters != nil { + in, out := &in.ExtraAuthorizeParameters, &out.ExtraAuthorizeParameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.URLs = in.URLs + in.Claims.DeepCopyInto(&out.Claims) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenIDIdentityProvider. +func (in *OpenIDIdentityProvider) DeepCopy() *OpenIDIdentityProvider { + if in == nil { + return nil + } + out := new(OpenIDIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenIDURLs) DeepCopyInto(out *OpenIDURLs) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenIDURLs. +func (in *OpenIDURLs) DeepCopy() *OpenIDURLs { + if in == nil { + return nil + } + out := new(OpenIDURLs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Project) DeepCopyInto(out *Project) { *out = *in @@ -1653,6 +2137,22 @@ func (in *ProjectStatus) DeepCopy() *ProjectStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyConfig) DeepCopyInto(out *ProxyConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfig. +func (in *ProxyConfig) DeepCopy() *ProxyConfig { + if in == nil { + return nil + } + out := new(ProxyConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegistryLocation) DeepCopyInto(out *RegistryLocation) { *out = *in @@ -1686,6 +2186,48 @@ func (in *RemoteConnectionInfo) DeepCopy() *RemoteConnectionInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestHeaderIdentityProvider) DeepCopyInto(out *RequestHeaderIdentityProvider) { + *out = *in + out.ClientCA = in.ClientCA + if in.ClientCommonNames != nil { + in, out := &in.ClientCommonNames, &out.ClientCommonNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PreferredUsernameHeaders != nil { + in, out := &in.PreferredUsernameHeaders, &out.PreferredUsernameHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NameHeaders != nil { + in, out := &in.NameHeaders, &out.NameHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EmailHeaders != nil { + in, out := &in.EmailHeaders, &out.EmailHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestHeaderIdentityProvider. +func (in *RequestHeaderIdentityProvider) DeepCopy() *RequestHeaderIdentityProvider { + if in == nil { + return nil + } + out := new(RequestHeaderIdentityProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Scheduling) DeepCopyInto(out *Scheduling) { *out = *in @@ -1841,6 +2383,22 @@ func (in *StringSourceSpec) DeepCopy() *StringSourceSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenConfig) DeepCopyInto(out *TokenConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenConfig. +func (in *TokenConfig) DeepCopy() *TokenConfig { + if in == nil { + return nil + } + out := new(TokenConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Update) DeepCopyInto(out *Update) { *out = *in @@ -1856,3 +2414,45 @@ func (in *Update) DeepCopy() *Update { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpdateHistory) DeepCopyInto(out *UpdateHistory) { + *out = *in + in.StartedTime.DeepCopyInto(&out.StartedTime) + if in.CompletionTime != nil { + in, out := &in.CompletionTime, &out.CompletionTime + if *in == nil { + *out = nil + } else { + *out = (*in).DeepCopy() + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateHistory. +func (in *UpdateHistory) DeepCopy() *UpdateHistory { + if in == nil { + return nil + } + out := new(UpdateHistory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookTokenAuthenticator) DeepCopyInto(out *WebhookTokenAuthenticator) { + *out = *in + out.KubeConfig = in.KubeConfig + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookTokenAuthenticator. +func (in *WebhookTokenAuthenticator) DeepCopy() *WebhookTokenAuthenticator { + if in == nil { + return nil + } + out := new(WebhookTokenAuthenticator) + in.DeepCopyInto(out) + return out +} From 558ae021bd79a781a9398e6c9ba9a0a9cdf52c92 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Fri, 30 Nov 2018 19:09:44 -0500 Subject: [PATCH 2/2] cvo: Record history of changes to version and report target version The CVO needs to clearly communicate to users what the recent changes to the version have been and what version it is targeting right now. Add a `history` field that tracks transitions in desiredUpdate, and a status field `desired` that reports when the operator is trying to reach a stable state. --- version: Allow user to set only version string and infer payload from history As a convenience to users and UI, allow only the version field to be specified in the desiredUpdate field. If a matching version exists in the history or the availableUpdates field, the update will proceed. If it does not, the CV will be marked as invalid. Take care to record the initial version in the history so the original payload is always available to recover if the user makes a mistake. Use a hash of the payload on disk rather than a version number to prevent collisions between mislabeled versions. --- ...ersion-operator_01_clusterversion.crd.yaml | 2 +- lib/validation/validation.go | 42 + pkg/cvo/cvo.go | 42 +- pkg/cvo/cvo_test.go | 759 ++++++++++++++++-- pkg/cvo/status.go | 113 ++- pkg/cvo/updatepayload.go | 35 +- 6 files changed, 873 insertions(+), 120 deletions(-) diff --git a/install/0000_00_cluster-version-operator_01_clusterversion.crd.yaml b/install/0000_00_cluster-version-operator_01_clusterversion.crd.yaml index 79f19cf800..6dbd7da830 100644 --- a/install/0000_00_cluster-version-operator_01_clusterversion.crd.yaml +++ b/install/0000_00_cluster-version-operator_01_clusterversion.crd.yaml @@ -28,7 +28,7 @@ spec: additionalPrinterColumns: - name: Version type: string - JSONPath: .status.current.version + JSONPath: .status.desired.version - name: Available type: string JSONPath: .status.conditions[?(@.type=="Available")].status diff --git a/lib/validation/validation.go b/lib/validation/validation.go index 0ed9b64d5e..a074b21030 100644 --- a/lib/validation/validation.go +++ b/lib/validation/validation.go @@ -36,11 +36,53 @@ func ValidateClusterVersion(config *configv1.ClusterVersion) field.ErrorList { errs = append(errs, field.Required(field.NewPath("spec", "desiredUpdate", "version"), "must specify version or payload")) case len(u.Version) > 0 && !validSemVer(u.Version): errs = append(errs, field.Invalid(field.NewPath("spec", "desiredUpdate", "version"), u.Version, "must be a semantic version (1.2.3[-...])")) + case len(u.Version) > 0 && len(u.Payload) == 0: + switch countPayloadsForVersion(config, u.Version) { + case 0: + errs = append(errs, field.Invalid(field.NewPath("spec", "desiredUpdate", "version"), u.Version, "when payload is empty the update must be a previous version or an available update")) + case 1: + default: + errs = append(errs, field.Invalid(field.NewPath("spec", "desiredUpdate", "version"), u.Version, "there are multiple possible payloads for this version, specify the exact payload")) + } } } return errs } +func countPayloadsForVersion(config *configv1.ClusterVersion, version string) int { + count := 0 + for _, update := range config.Status.AvailableUpdates { + if update.Version == version && len(update.Payload) > 0 { + count++ + } + } + if count > 0 { + return count + } + for _, history := range config.Status.History { + if history.Version == version { + if len(history.Payload) > 0 { + return 1 + } + } + } + return 0 +} + +func hasAmbiguousPayloadForVersion(config *configv1.ClusterVersion, version string) bool { + for _, update := range config.Status.AvailableUpdates { + if update.Version == version { + return len(update.Payload) > 0 + } + } + for _, history := range config.Status.History { + if history.Version == version { + return len(history.Payload) > 0 + } + } + return false +} + func ClearInvalidFields(config *configv1.ClusterVersion, errs field.ErrorList) *configv1.ClusterVersion { if len(errs) == 0 { return config diff --git a/pkg/cvo/cvo.go b/pkg/cvo/cvo.go index ce3261bd52..8330c8a6d5 100644 --- a/pkg/cvo/cvo.go +++ b/pkg/cvo/cvo.go @@ -324,7 +324,7 @@ func (optr *Operator) sync(key string) error { glog.V(2).Infof("Reconciling cluster to version %s and image %s (hash=%s)", update.Version, update.Payload, payload.ManifestHash) } else { glog.V(2).Infof("Updating the cluster to version %s and image %s (hash=%s)", update.Version, update.Payload, payload.ManifestHash) - if err := optr.syncProgressingStatus(original); err != nil { + if err := optr.syncProgressingStatus(original, update); err != nil { return err } } @@ -429,7 +429,6 @@ func (optr *Operator) getOrCreateClusterVersion() (*configv1.ClusterVersion, boo // for fields that have meaning that are incomplete, clear them // prevents us from loading clearly malformed payloads obj = validation.ClearInvalidFields(obj, errs) - return obj, changed, nil } @@ -465,11 +464,14 @@ func (optr *Operator) getOrCreateClusterVersion() (*configv1.ClusterVersion, boo // versionString returns a string describing the current version. func (optr *Operator) currentVersionString(config *configv1.ClusterVersion) string { - if s := config.Status.Current.Version; len(s) > 0 { - return s - } - if s := config.Status.Current.Payload; len(s) > 0 { - return s + if len(config.Status.History) > 0 { + last := config.Status.History[0] + if s := last.Version; len(s) > 0 { + return s + } + if s := last.Payload; len(s) > 0 { + return s + } } if s := optr.releaseVersion; len(s) > 0 { return s @@ -480,21 +482,23 @@ func (optr *Operator) currentVersionString(config *configv1.ClusterVersion) stri return "" } +// desiredVersion returns the update that is currently targeted by the config. +func (optr *Operator) desiredVersion(config *configv1.ClusterVersion) configv1.Update { + if update := config.Spec.DesiredUpdate; update != nil { + return *update + } + return optr.currentVersion() +} + // versionString returns a string describing the desired version. -func (optr *Operator) desiredVersionString(config *configv1.ClusterVersion) string { - var s string - if v := config.Spec.DesiredUpdate; v != nil { - if len(v.Payload) > 0 { - s = v.Payload - } - if len(v.Version) > 0 { - s = v.Version - } +func versionString(update configv1.Update) string { + if len(update.Version) > 0 { + return update.Version } - if len(s) == 0 { - s = optr.currentVersionString(config) + if len(update.Payload) > 0 { + return update.Payload } - return s + return "" } // currentVersion returns an update object describing the current known cluster version. diff --git a/pkg/cvo/cvo_test.go b/pkg/cvo/cvo_test.go index b999f94d35..b6a57e5bbc 100644 --- a/pkg/cvo/cvo_test.go +++ b/pkg/cvo/cvo_test.go @@ -203,6 +203,20 @@ func TestOperator_sync(t *testing.T) { `, }, } + content_4_0_1 := map[string]interface{}{ + "manifests": map[string]interface{}{}, + "release-manifests": map[string]interface{}{ + "image-references": ` + { + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "4.0.1" + } + } + `, + }, + } tests := []struct { name string @@ -259,10 +273,10 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "4.0.1", Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, }, + Desired: configv1.Update{Version: "4.0.1", Payload: "payload/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -289,15 +303,16 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "0.0.1-abc", Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + {Version: "4.0.1", Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}, CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}}, }, + Desired: configv1.Update{Version: "0.0.1-abc", Payload: "payload/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, {Type: configv1.OperatorFailing, Status: configv1.ConditionTrue, Reason: "UpdatePayloadIntegrity", Message: "unable to apply object"}, - {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: "UpdatePayloadIntegrity", Message: "Unable to apply 4.0.1: the contents of the update are invalid"}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: "UpdatePayloadIntegrity", Message: "Unable to apply 0.0.1-abc: the contents of the update are invalid"}, {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, }, }, @@ -311,7 +326,22 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ + History: []configv1.UpdateHistory{ + { + State: configv1.CompletedUpdate, + Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + { + Version: "4.0.1", + Payload: "payload/image:v4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", Payload: "payload/image:v4.0.1", }, @@ -346,9 +376,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "4.0.1", Payload: "payload/image:v4.0.1"}, }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -381,9 +410,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "4.0.1", Payload: "payload/image:v4.0.1"}, }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -402,9 +430,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "0.0.1-abc", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "0.0.1-abc", Payload: "payload/image:v4.0.1"}, }, VersionHash: "y_Kc5IQiIyU=", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -456,9 +483,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "4.0.1", Payload: "payload/image:v4.0.1"}, }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -477,9 +503,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "0.0.1-abc", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "0.0.1-abc", Payload: "payload/image:v4.0.1"}, }, VersionHash: "y_Kc5IQiIyU=", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -508,9 +533,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "4.0.1", Payload: "payload/image:v4.0.1"}, }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -541,9 +565,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "4.0.1", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "4.0.1", Payload: "payload/image:v4.0.1"}, }, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -562,9 +585,8 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Version: "0.0.1-abc", - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Version: "0.0.1-abc", Payload: "payload/image:v4.0.1"}, }, VersionHash: "y_Kc5IQiIyU=", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -612,9 +634,15 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + Version: "", // we don't know our payload yet and releaseVersion is unset + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -627,7 +655,340 @@ func TestOperator_sync(t *testing.T) { }, }, { - name: "after initial status is set, set reconciling and hash", + name: "record a new version entry if the controller is restarted with a new image", + content: content1, + optr: Operator{ + releaseImage: "payload/image:v4.0.2", + releaseVersion: "4.0.2", + namespace: "test", + name: "default", + client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "1", + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + Version: "", // we didn't know our payload before + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, + }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1"}, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Initializing, will work towards payload/image:v4.0.1"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }), + }, + wantActions: func(t *testing.T, optr *Operator) { + f := optr.client.(*fake.Clientset) + act := f.Actions() + if len(act) != 2 { + t.Fatalf("unknown actions: %d %#v", len(act), act) + } + expectGet(t, act[0], "clusterversions", "", "default") + expectUpdateStatus(t, act[1], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "1", + }, + Spec: configv1.ClusterVersionSpec{ + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.2", + Version: "4.0.2", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + Version: "", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{Payload: "payload/image:v4.0.2", Version: "4.0.2"}, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Initializing, will work towards payload/image:v4.0.1"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }) + }, + }, + { + name: "when user cancels desired update, clear status desired", + content: content1, + optr: Operator{ + releaseImage: "payload/image:v4.0.1", + releaseVersion: "4.0.1", + namespace: "test", + name: "default", + client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "1", + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.2", + Version: "4.0.2", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{Payload: "payload/image:v4.0.2"}, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Working towards 4.0.2"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }), + }, + wantActions: func(t *testing.T, optr *Operator) { + f := optr.client.(*fake.Clientset) + act := f.Actions() + if len(act) != 2 { + t.Fatalf("unknown actions: %d %#v", len(act), act) + } + expectGet(t, act[0], "clusterversions", "", "default") + expectUpdateStatus(t, act[1], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "1", + }, + Spec: configv1.ClusterVersionSpec{ + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.2", + Version: "4.0.2", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ + Version: "4.0.1", + Payload: "payload/image:v4.0.1", + }, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + // we don't reset the message here until the payload is loaded + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Working towards 4.0.2"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }) + }, + }, + { + name: "after desired update is cancelled, go to reconciling", + content: content_4_0_1, + optr: Operator{ + releaseImage: "payload/image:v4.0.1", + releaseVersion: "4.0.1", + namespace: "test", + name: "default", + client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "1", + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.2", + Version: "4.0.2", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1", Version: "4.0.1"}, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + // we don't reset the message here until the payload is loaded + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Working towards 4.0.2"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }), + }, + wantActions: func(t *testing.T, optr *Operator) { + f := optr.client.(*fake.Clientset) + act := f.Actions() + if len(act) != 3 { + t.Fatalf("unknown actions: %d %#v", len(act), act) + } + expectGet(t, act[0], "clusterversions", "", "default") + expectUpdateStatus(t, act[1], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "1", + }, + Spec: configv1.ClusterVersionSpec{ + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.2", + Version: "4.0.2", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1", Version: "4.0.1"}, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + // we correct the message that was incorrect from the previous state + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Working towards 4.0.1"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }) + expectUpdateStatus(t, act[2], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "2", + }, + Spec: configv1.ClusterVersionSpec{ + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.2", + Version: "4.0.2", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ + Version: "4.0.1", + Payload: "payload/image:v4.0.1", + }, + VersionHash: "y_Kc5IQiIyU=", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying 4.0.1"}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Message: "Cluster version is 4.0.1"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }) + }, + }, + { + name: "after initial status is set, set reconciling and hash and correct version number", content: content1, optr: Operator{ releaseImage: "payload/image:v4.0.1", @@ -644,14 +1005,19 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + { + State: configv1.PartialUpdate, + Payload: "payload/image:v4.0.1", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + }, }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, - {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Working towards payload/image:v4.0.1"}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Initializing, will work towards payload/image:v4.0.1"}, {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, }, }, @@ -660,10 +1026,11 @@ func TestOperator_sync(t *testing.T) { wantActions: func(t *testing.T, optr *Operator) { f := optr.client.(*fake.Clientset) act := f.Actions() - if len(act) != 2 { + if len(act) != 3 { t.Fatalf("unknown actions: %d %#v", len(act), act) } expectGet(t, act[0], "clusterversions", "", "default") + // will use the version from content1 (the payload) when we set the progressing condition expectUpdateStatus(t, act[1], "clusterversions", "", &configv1.ClusterVersion{ ObjectMeta: metav1.ObjectMeta{ Name: "default", @@ -674,10 +1041,35 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", - // loads the version from the payload on disk + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", Version: "0.0.1-abc", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1", Version: "0.0.1-abc"}, + VersionHash: "", + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Message: "Working towards 0.0.1-abc"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + }, + }, + }) + expectUpdateStatus(t, act[2], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + ResourceVersion: "2", + }, + Spec: configv1.ClusterVersionSpec{ + Upstream: configv1.URL("http://localhost:8080/graph"), + Channel: "fast", + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + {State: configv1.CompletedUpdate, Payload: "payload/image:v4.0.1", Version: "0.0.1-abc", StartedTime: metav1.Time{Time: time.Unix(1, 0)}, CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}}, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", }, VersionHash: "y_Kc5IQiIyU=", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -712,9 +1104,17 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "0.0.1-abc", + CompletionTime: &metav1.Time{Time: time.Unix(1, 0)}, + }, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", }, Generation: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -800,7 +1200,12 @@ func TestOperator_sync(t *testing.T) { {Version: "4.0.2", Payload: "test/image:1"}, {Version: "4.0.3", Payload: "test/image:2"}, }, - Current: configv1.Update{Payload: "payload/image:v4.0.1"}, + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + }, + Desired: configv1.Update{ + Payload: "payload/image:v4.0.1", + }, Generation: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -877,7 +1282,12 @@ func TestOperator_sync(t *testing.T) { {Version: "4.0.2", Payload: "test/image:1"}, {Version: "4.0.3", Payload: "test/image:2"}, }, - Current: configv1.Update{Payload: "payload/image:v4.0.1"}, + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + }, + Desired: configv1.Update{ + Payload: "payload/image:v4.0.1", + }, Generation: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -949,7 +1359,12 @@ func TestOperator_sync(t *testing.T) { Channel: "", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{Payload: "payload/image:v4.0.1"}, + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + }, + Desired: configv1.Update{ + Payload: "payload/image:v4.0.1", + }, Generation: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -961,6 +1376,146 @@ func TestOperator_sync(t *testing.T) { }) }, }, + { + name: "user requested a version that isn't in the updates or history", + content: content1, + optr: Operator{ + releaseImage: "payload/image:v4.0.1", + namespace: "test", + name: "default", + client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Generation: 2, + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + DesiredUpdate: &configv1.Update{ + Version: "4.0.4", + }, + }, + Status: configv1.ClusterVersionStatus{ + AvailableUpdates: []configv1.Update{ + {Version: "4.0.2", Payload: "test/image:1"}, + {Version: "4.0.3", Payload: "test/image:2"}, + }, + }, + }), + }, + wantActions: func(t *testing.T, optr *Operator) { + f := optr.client.(*fake.Clientset) + act := f.Actions() + if len(act) != 2 { + t.Fatalf("unknown actions: %d %#v", len(act), act) + } + expectGet(t, act[0], "clusterversions", "", "default") + expectUpdateStatus(t, act[1], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Generation: 2, + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + DesiredUpdate: &configv1.Update{ + Version: "4.0.4", + }, + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "4.0.4", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}, CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}}, + }, + Desired: configv1.Update{ + Version: "4.0.4", + }, + AvailableUpdates: []configv1.Update{ + {Version: "4.0.2", Payload: "test/image:1"}, + {Version: "4.0.3", Payload: "test/image:2"}, + }, + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Reason: "InvalidClusterVersion", Message: "Stopped at payload/image:v4.0.1: the cluster version is invalid"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + {Type: ClusterVersionInvalid, Status: configv1.ConditionTrue, Reason: "InvalidClusterVersion", Message: "The cluster version is invalid: spec.desiredUpdate.version: Invalid value: \"4.0.4\": when payload is empty the update must be a previous version or an available update"}, + }, + }, + }) + }, + }, + { + name: "user requested a version has duplicates", + content: content1, + optr: Operator{ + releaseImage: "payload/image:v4.0.1", + namespace: "test", + name: "default", + client: fakeClientsetWithUpdates(&configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Generation: 2, + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + DesiredUpdate: &configv1.Update{ + Version: "4.0.3", + }, + }, + Status: configv1.ClusterVersionStatus{ + AvailableUpdates: []configv1.Update{ + {Version: "4.0.2", Payload: "test/image:1"}, + {Version: "4.0.3", Payload: "test/image:2"}, + {Version: "4.0.3", Payload: "test/image:3"}, + }, + }, + }), + }, + wantActions: func(t *testing.T, optr *Operator) { + f := optr.client.(*fake.Clientset) + act := f.Actions() + if len(act) != 2 { + t.Fatalf("unknown actions: %d %#v", len(act), act) + } + expectGet(t, act[0], "clusterversions", "", "default") + expectUpdateStatus(t, act[1], "clusterversions", "", &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Generation: 2, + }, + Spec: configv1.ClusterVersionSpec{ + ClusterID: configv1.ClusterID(id), + Upstream: configv1.URL("http://localhost:8080/graph"), + DesiredUpdate: &configv1.Update{ + Version: "4.0.3", + }, + }, + Status: configv1.ClusterVersionStatus{ + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Version: "4.0.3", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}, CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}}, + }, + Desired: configv1.Update{ + Version: "4.0.3", + }, + AvailableUpdates: []configv1.Update{ + {Version: "4.0.2", Payload: "test/image:1"}, + {Version: "4.0.3", Payload: "test/image:2"}, + {Version: "4.0.3", Payload: "test/image:3"}, + }, + Conditions: []configv1.ClusterOperatorStatusCondition{ + {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Reason: "InvalidClusterVersion", Message: "Stopped at payload/image:v4.0.1: the cluster version is invalid"}, + {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, + {Type: ClusterVersionInvalid, Status: configv1.ConditionTrue, Reason: "InvalidClusterVersion", Message: "The cluster version is invalid: spec.desiredUpdate.version: Invalid value: \"4.0.3\": there are multiple possible payloads for this version, specify the exact payload"}, + }, + }, + }) + }, + }, { name: "payload hash matches content hash, act as reconcile, no need to apply", content: content1, @@ -981,10 +1536,18 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ // loads the version from the payload on disk + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "0.0.1-abc", + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", }, VersionHash: "y_Kc5IQiIyU=", Generation: 2, @@ -1027,10 +1590,18 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ // loads the version from the payload on disk + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "0.0.1-abc", + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", }, VersionHash: "unknown_hash", Generation: 2, @@ -1061,10 +1632,15 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", - Version: "0.0.1-abc", + History: []configv1.UpdateHistory{ + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "0.0.1-abc", + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, }, + Desired: configv1.Update{Version: "0.0.1-abc", Payload: "payload/image:v4.0.1"}, Generation: 2, VersionHash: "unknown_hash", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -1085,10 +1661,17 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", - // loads the version from the payload on disk + History: []configv1.UpdateHistory{ + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "0.0.1-abc", + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", }, Generation: 2, VersionHash: "y_Kc5IQiIyU=", @@ -1140,7 +1723,10 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ + History: []configv1.UpdateHistory{ + {State: configv1.PartialUpdate, Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, + }, + Desired: configv1.Update{ Payload: "payload/image:v4.0.1", }, VersionHash: "", @@ -1174,9 +1760,10 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, @@ -1208,14 +1795,15 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1", Version: "0.0.1-abc", StartedTime: metav1.Time{Time: time.Unix(1, 0)}}, }, + Desired: configv1.Update{Payload: "payload/image:v4.0.1", Version: "0.0.1-abc"}, VersionHash: "", Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse}, {Type: configv1.OperatorFailing, Status: configv1.ConditionFalse}, - {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: "", Message: "Reconciling payload/image:v4.0.1: the cluster version is invalid"}, + {Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: "", Message: "Reconciling 0.0.1-abc: the cluster version is invalid"}, {Type: configv1.RetrievedUpdates, Status: configv1.ConditionFalse}, {Type: ClusterVersionInvalid, Status: configv1.ConditionTrue, Reason: "InvalidClusterVersion", Message: "The cluster version is invalid:\n* spec.upstream: Invalid value: \"#%GG\": must be a valid URL or empty\n* spec.clusterID: Invalid value: \"not-valid-cluster-id\": must be an RFC4122-variant UUID\n"}, }, @@ -1234,9 +1822,18 @@ func TestOperator_sync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + { + State: configv1.CompletedUpdate, + Payload: "payload/image:v4.0.1", + Version: "0.0.1-abc", + StartedTime: metav1.Time{Time: time.Unix(1, 0)}, + CompletionTime: &metav1.Time{Time: time.Unix(2, 0)}, + }, + }, + Desired: configv1.Update{ Version: "0.0.1-abc", + Payload: "payload/image:v4.0.1", }, VersionHash: "y_Kc5IQiIyU=", Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -1331,8 +1928,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1"}, }, }, }, @@ -1370,8 +1967,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1"}, }, }, }, @@ -1409,8 +2006,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1"}, }, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying payload/image:v4.0.1"}, @@ -1453,8 +2050,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1"}, }, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying payload/image:v4.0.1"}, @@ -1509,8 +2106,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1"}, }, Conditions: []configv1.ClusterOperatorStatusCondition{ {Type: configv1.OperatorAvailable, Status: configv1.ConditionTrue, Message: "Done applying payload/image:v4.0.1"}, @@ -1564,8 +2161,8 @@ func TestOperator_availableUpdatesSync(t *testing.T) { Channel: "fast", }, Status: configv1.ClusterVersionStatus{ - Current: configv1.Update{ - Payload: "payload/image:v4.0.1", + History: []configv1.UpdateHistory{ + {Payload: "payload/image:v4.0.1"}, }, Generation: 2, Conditions: []configv1.ClusterOperatorStatusCondition{ @@ -1708,6 +2305,14 @@ func expectMutation(t *testing.T, a ktesting.Action, verb string, resource, subr for i := range in.Status.Conditions { in.Status.Conditions[i].LastTransitionTime.Time = time.Time{} } + for i, item := range in.Status.History { + if !item.StartedTime.IsZero() { + in.Status.History[i].StartedTime.Time = time.Unix(1, 0) + } + if item.CompletionTime != nil { + in.Status.History[i].CompletionTime.Time = time.Unix(2, 0) + } + } } e, a := fmt.Sprintf("%s/%s", resource, namespace), fmt.Sprintf("%s/%s", at.GetResource().Resource, at.GetNamespace()) diff --git a/pkg/cvo/status.go b/pkg/cvo/status.go index 9d414de165..8085f345ee 100644 --- a/pkg/cvo/status.go +++ b/pkg/cvo/status.go @@ -15,6 +15,68 @@ import ( "github.com/openshift/cluster-version-operator/lib/resourcemerge" ) +func mergeEqualVersions(current *configv1.UpdateHistory, desired *configv1.Update) bool { + if len(desired.Payload) > 0 && desired.Payload == current.Payload { + if len(current.Version) == 0 || desired.Version == current.Version { + current.Version = desired.Version + return true + } + } + if len(desired.Version) > 0 && desired.Version == current.Version { + if len(current.Payload) == 0 || desired.Payload == current.Payload { + current.Payload = desired.Payload + return true + } + } + return false +} + +func mergeOperatorHistory(config *configv1.ClusterVersion, current configv1.Update, now metav1.Time) { + if len(config.Status.History) == 0 { + config.Status.History = append(config.Status.History, configv1.UpdateHistory{ + Version: current.Version, + Payload: current.Payload, + + State: configv1.PartialUpdate, + StartedTime: now, + }) + } + + last := &config.Status.History[0] + desired := config.Spec.DesiredUpdate + if desired == nil { + desired = ¤t + } + + if !mergeEqualVersions(last, desired) { + last.CompletionTime = &now + config.Status.History = append([]configv1.UpdateHistory{ + { + Version: desired.Version, + Payload: desired.Payload, + + State: configv1.PartialUpdate, + StartedTime: now, + }, + }, config.Status.History...) + last = &config.Status.History[0] + } + + if len(config.Status.History) > 10 { + config.Status.History = config.Status.History[:10] + } + + switch { + case resourcemerge.IsOperatorStatusConditionTrue(config.Status.Conditions, configv1.OperatorAvailable): + last.State = configv1.CompletedUpdate + if last.CompletionTime == nil { + last.CompletionTime = &now + } + } + + config.Status.Desired = *desired +} + // ClusterVersionInvalid indicates that the cluster version has an error that prevents the server from // taking action. The cluster version operator will only reconcile the current state as long as this // condition is set. @@ -25,12 +87,13 @@ const ClusterVersionInvalid configv1.ClusterStatusConditionType = "Invalid" func (optr *Operator) syncInitialObjectStatus(original *configv1.ClusterVersion, errs field.ErrorList) (bool, error) { config := original.DeepCopy() - config.Status.Current = optr.currentVersion() if updated := optr.getAvailableUpdates().NeedsUpdate(config); updated != nil { config = updated } now := metav1.Now() + target := optr.desiredVersion(original) + current := optr.currentVersion() // ensure the initial state of all conditions is set if resourcemerge.FindOperatorStatusCondition(config.Status.Conditions, configv1.OperatorAvailable) == nil { @@ -44,7 +107,7 @@ func (optr *Operator) syncInitialObjectStatus(original *configv1.ClusterVersion, resourcemerge.SetOperatorStatusCondition(&config.Status.Conditions, configv1.ClusterOperatorStatusCondition{ Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, - Message: fmt.Sprintf("Initializing, will work towards %s", optr.desiredVersionString(config)), + Message: fmt.Sprintf("Initializing, will work towards %s", versionString(target)), LastTransitionTime: now, }) } @@ -79,7 +142,7 @@ func (optr *Operator) syncInitialObjectStatus(original *configv1.ClusterVersion, Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Reason: reason, - Message: fmt.Sprintf("Stopped at %s: the cluster version is invalid", optr.desiredVersionString(config)), + Message: fmt.Sprintf("Stopped at %s: the cluster version is invalid", versionString(current)), LastTransitionTime: now, }) } @@ -88,16 +151,21 @@ func (optr *Operator) syncInitialObjectStatus(original *configv1.ClusterVersion, resourcemerge.RemoveOperatorStatusCondition(&config.Status.Conditions, ClusterVersionInvalid) } + // ensure we record the initial state so the user can roll back + if len(config.Status.History) == 0 { + mergeOperatorHistory(config, current, now) + } + mergeOperatorHistory(config, target, now) + updated, err := applyClusterVersionStatus(optr.client.ConfigV1(), config, original) optr.rememberLastUpdate(updated) return updated != nil && updated.ResourceVersion != original.ResourceVersion, err } -func (optr *Operator) syncProgressingStatus(config *configv1.ClusterVersion) error { +func (optr *Operator) syncProgressingStatus(config *configv1.ClusterVersion, update configv1.Update) error { original := config.DeepCopy() config.Status.Generation = config.Generation - config.Status.Current = optr.currentVersion() now := metav1.Now() @@ -117,25 +185,27 @@ func (optr *Operator) syncProgressingStatus(config *configv1.ClusterVersion) err Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: reason, - Message: fmt.Sprintf("Unable to apply %s: %s", optr.desiredVersionString(config), msg), + Message: fmt.Sprintf("Unable to apply %s: %s", versionString(update), msg), LastTransitionTime: now, }) } else if resourcemerge.IsOperatorStatusConditionTrue(config.Status.Conditions, ClusterVersionInvalid) { resourcemerge.SetOperatorStatusCondition(&config.Status.Conditions, configv1.ClusterOperatorStatusCondition{ Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, - Message: fmt.Sprintf("Reconciling %s: the cluster version is invalid", optr.desiredVersionString(config)), + Message: fmt.Sprintf("Reconciling %s: the cluster version is invalid", versionString(update)), LastTransitionTime: now, }) } else { resourcemerge.SetOperatorStatusCondition(&config.Status.Conditions, configv1.ClusterOperatorStatusCondition{ Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, - Message: fmt.Sprintf("Working towards %s", optr.desiredVersionString(config)), + Message: fmt.Sprintf("Working towards %s", versionString(update)), LastTransitionTime: now, }) } + mergeOperatorHistory(config, update, now) + updated, err := applyClusterVersionStatus(optr.client.ConfigV1(), config, original) optr.rememberLastUpdate(updated) return err @@ -144,7 +214,6 @@ func (optr *Operator) syncProgressingStatus(config *configv1.ClusterVersion) err func (optr *Operator) syncAvailableStatus(config *configv1.ClusterVersion, current configv1.Update, versionHash string) error { original := config.DeepCopy() - config.Status.Current = current config.Status.VersionHash = versionHash config.Status.Generation = config.Generation @@ -169,7 +238,7 @@ func (optr *Operator) syncAvailableStatus(config *configv1.ClusterVersion, curre Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Reason: "InvalidClusterVersion", - Message: fmt.Sprintf("Stopped at %s: the cluster version is invalid", optr.desiredVersionString(config)), + Message: fmt.Sprintf("Stopped at %s: the cluster version is invalid", versionString(current)), LastTransitionTime: now, }) } else { @@ -182,6 +251,8 @@ func (optr *Operator) syncAvailableStatus(config *configv1.ClusterVersion, curre }) } + mergeOperatorHistory(config, current, now) + updated, err := applyClusterVersionStatus(optr.client.ConfigV1(), config, original) optr.rememberLastUpdate(updated) return err @@ -191,7 +262,6 @@ func (optr *Operator) syncPayloadFailingStatus(original *configv1.ClusterVersion config := original.DeepCopy() config.Status.Generation = config.Generation - config.Status.Current = optr.currentVersion() now := metav1.Now() var reason string @@ -201,6 +271,8 @@ func (optr *Operator) syncPayloadFailingStatus(original *configv1.ClusterVersion msg = summaryForReason(reason) } + target := optr.desiredVersion(original) + // leave the available condition alone // set the failing condition @@ -218,7 +290,7 @@ func (optr *Operator) syncPayloadFailingStatus(original *configv1.ClusterVersion Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: reason, - Message: fmt.Sprintf("Unable to apply %s: %s", optr.desiredVersionString(config), msg), + Message: fmt.Sprintf("Unable to apply %s: %s", versionString(target), msg), LastTransitionTime: now, }) } else { @@ -226,11 +298,13 @@ func (optr *Operator) syncPayloadFailingStatus(original *configv1.ClusterVersion Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Reason: reason, - Message: fmt.Sprintf("Error while reconciling %s: %s", optr.desiredVersionString(config), msg), + Message: fmt.Sprintf("Error while reconciling %s: %s", versionString(target), msg), LastTransitionTime: now, }) } + mergeOperatorHistory(config, target, now) + updated, err := applyClusterVersionStatus(optr.client.ConfigV1(), config, original) optr.rememberLastUpdate(updated) return err @@ -240,7 +314,6 @@ func (optr *Operator) syncUpdateFailingStatus(original *configv1.ClusterVersion, config := original.DeepCopy() config.Status.Generation = config.Generation - config.Status.Current = optr.currentVersion() now := metav1.Now() var reason string @@ -250,6 +323,8 @@ func (optr *Operator) syncUpdateFailingStatus(original *configv1.ClusterVersion, msg = summaryForReason(reason) } + target := optr.desiredVersion(original) + // clear the available condition resourcemerge.SetOperatorStatusCondition(&config.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorAvailable, Status: configv1.ConditionFalse, LastTransitionTime: now}) @@ -268,7 +343,7 @@ func (optr *Operator) syncUpdateFailingStatus(original *configv1.ClusterVersion, Type: configv1.OperatorProgressing, Status: configv1.ConditionTrue, Reason: reason, - Message: fmt.Sprintf("Unable to apply %s: %s", optr.desiredVersionString(config), msg), + Message: fmt.Sprintf("Unable to apply %s: %s", versionString(target), msg), LastTransitionTime: now, }) } else { @@ -276,11 +351,13 @@ func (optr *Operator) syncUpdateFailingStatus(original *configv1.ClusterVersion, Type: configv1.OperatorProgressing, Status: configv1.ConditionFalse, Reason: reason, - Message: fmt.Sprintf("Error while reconciling %s: %s", optr.desiredVersionString(config), msg), + Message: fmt.Sprintf("Error while reconciling %s: %s", versionString(target), msg), LastTransitionTime: now, }) } + mergeOperatorHistory(config, target, now) + updated, err := applyClusterVersionStatus(optr.client.ConfigV1(), config, original) optr.rememberLastUpdate(updated) return err @@ -310,8 +387,6 @@ func (optr *Operator) syncFailingStatus(config *configv1.ClusterVersion, ierr er original := config.DeepCopy() - config.Status.Current = optr.currentVersion() - now := metav1.Now() msg := fmt.Sprintf("Error ensuring the cluster version is up to date: %v", ierr) @@ -338,6 +413,8 @@ func (optr *Operator) syncFailingStatus(config *configv1.ClusterVersion, ierr er LastTransitionTime: now, }) + mergeOperatorHistory(config, optr.currentVersion(), now) + updated, err := applyClusterVersionStatus(optr.client.ConfigV1(), config, original) optr.rememberLastUpdate(updated) if err != nil { diff --git a/pkg/cvo/updatepayload.go b/pkg/cvo/updatepayload.go index c28f464c80..d2605a0643 100644 --- a/pkg/cvo/updatepayload.go +++ b/pkg/cvo/updatepayload.go @@ -2,6 +2,7 @@ package cvo import ( "bytes" + "crypto/md5" "encoding/base64" "fmt" "hash/fnv" @@ -183,11 +184,15 @@ func (optr *Operator) updatePayloadDir(config *configv1.ClusterVersion) (string, } func (optr *Operator) targetUpdatePayloadDir(config *configv1.ClusterVersion) (string, error) { - if !isTargetSet(config.Spec.DesiredUpdate) { + payload, ok := findUpdatePayload(config) + if !ok { return "", nil } + hash := md5.New() + hash.Write([]byte(payload)) + payloadHash := base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) - tdir := filepath.Join(targetUpdatePayloadsDir, config.Spec.DesiredUpdate.Version) + tdir := filepath.Join(targetUpdatePayloadsDir, payloadHash) err := validateUpdatePayload(tdir) if os.IsNotExist(err) { // the dirs don't exist, try fetching the payload to tdir. @@ -313,7 +318,27 @@ func copyPayloadCmd(tdir string) string { return fmt.Sprintf("%s && %s", cvoCmd, releaseCmd) } -func isTargetSet(desired *configv1.Update) bool { - return desired != nil && desired.Payload != "" && - desired.Version != "" +func findUpdatePayload(config *configv1.ClusterVersion) (string, bool) { + update := config.Spec.DesiredUpdate + if update == nil { + return "", false + } + if len(update.Payload) == 0 { + return findPayloadForVersion(config, update.Version) + } + return update.Payload, len(update.Payload) > 0 +} + +func findPayloadForVersion(config *configv1.ClusterVersion, version string) (string, bool) { + for _, update := range config.Status.AvailableUpdates { + if update.Version == version { + return update.Payload, len(update.Payload) > 0 + } + } + for _, history := range config.Status.History { + if history.Version == version { + return history.Payload, len(history.Payload) > 0 + } + } + return "", false }