diff --git a/build/components/versions.yml b/build/components/versions.yml index 82edecc82c..60f95ba1bb 100644 --- a/build/components/versions.yml +++ b/build/components/versions.yml @@ -4,7 +4,6 @@ firmware: edk2: stable202411 core: 3p-kubevirt: v1.6.2-v12n.35 - 3p-containerized-data-importer: v1.60.3-v12n.19 distribution: 2.8.3 package: acl: v2.3.1 diff --git a/crds/embedded/cdi.yaml b/crds/embedded/cdi.yaml deleted file mode 100644 index 05dd7321a4..0000000000 --- a/crds/embedded/cdi.yaml +++ /dev/null @@ -1,2324 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: internalvirtualizationcdis.cdi.internal.virtualization.deckhouse.io - labels: - heritage: deckhouse - module: virtualization - app.kubernetes.io/component: cdi -spec: - group: cdi.internal.virtualization.deckhouse.io - names: - kind: InternalVirtualizationCDI - listKind: InternalVirtualizationCDIList - plural: internalvirtualizationcdis - shortNames: - - intvirtcdi - singular: internalvirtualizationcdi - scope: Cluster - versions: - - additionalPrinterColumns: - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - - jsonPath: .status.phase - name: Phase - type: string - name: v1beta1 - schema: - openAPIV3Schema: - description: CDI is the CDI Operator CRD - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: CDISpec defines our specification for the CDI installation - properties: - certConfig: - description: certificate configuration - properties: - ca: - description: CA configuration CA certs are kept in the CA bundle - as long as they are valid - properties: - duration: - description: The requested 'duration' (i.e. lifetime) of the - Certificate. - type: string - renewBefore: - description: The amount of time before the currently issued - certificate's `notAfter` time that we will begin to attempt - to renew the certificate. - type: string - type: object - server: - description: Server configuration Certs are rotated and discarded - properties: - duration: - description: The requested 'duration' (i.e. lifetime) of the - Certificate. - type: string - renewBefore: - description: The amount of time before the currently issued - certificate's `notAfter` time that we will begin to attempt - to renew the certificate. - type: string - type: object - type: object - cloneStrategyOverride: - description: 'Clone strategy override: should we use a host-assisted - copy even if snapshots are available?' - enum: - - copy - - snapshot - - csi-clone - type: string - config: - description: CDIConfig at CDI level - properties: - dataVolumeTTLSeconds: - description: DataVolumeTTLSeconds is the time in seconds after - DataVolume completion it can be garbage collected. Disabled - by default. - format: int32 - type: integer - featureGates: - description: FeatureGates are a list of specific enabled feature - gates - items: - type: string - type: array - filesystemOverhead: - description: FilesystemOverhead describes the space reserved for - overhead when using Filesystem volumes. A value is between 0 - and 1, if not defined it is 0.055 (5.5% overhead) - properties: - global: - description: Global is how much space of a Filesystem volume - should be reserved for overhead. This value is used unless - overridden by a more specific value (per storageClass) - pattern: ^(0(?:\.\d{1,3})?|1)$ - type: string - storageClass: - additionalProperties: - description: 'Percent is a string that can only be a value - between [0,1) (Note: we actually rely on reconcile to - reject invalid values)' - pattern: ^(0(?:\.\d{1,3})?|1)$ - type: string - description: StorageClass specifies how much space of a Filesystem - volume should be reserved for safety. The keys are the storageClass - and the values are the overhead. This value overrides the - global value - type: object - type: object - imagePullSecrets: - description: The imagePullSecrets used to pull the container images - items: - description: LocalObjectReference contains enough information - to let you locate the referenced object inside the same namespace. - properties: - name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - type: string - type: object - x-kubernetes-map-type: atomic - type: array - importProxy: - description: ImportProxy contains importer pod proxy configuration. - properties: - HTTPProxy: - description: HTTPProxy is the URL http://:@: - of the import proxy for HTTP requests. Empty means unset - and will not result in the import pod env var. - type: string - HTTPSProxy: - description: HTTPSProxy is the URL https://:@: - of the import proxy for HTTPS requests. Empty means unset - and will not result in the import pod env var. - type: string - noProxy: - description: NoProxy is a comma-separated list of hostnames - and/or CIDRs for which the proxy should not be used. Empty - means unset and will not result in the import pod env var. - type: string - trustedCAProxy: - description: "TrustedCAProxy is the name of a ConfigMap in - the cdi namespace that contains a user-provided trusted - certificate authority (CA) bundle. The TrustedCAProxy ConfigMap - is consumed by the DataImportCron controller for creating - cronjobs, and by the import controller referring a copy - of the ConfigMap in the import namespace. Here is an example - of the ConfigMap (in yaml): \n apiVersion: v1 kind: ConfigMap - metadata: name: my-ca-proxy-cm namespace: cdi data: ca.pem: - | -----BEGIN CERTIFICATE----- ... - ... -----END CERTIFICATE-----" - type: string - type: object - insecureRegistries: - description: InsecureRegistries is a list of TLS disabled registries - items: - type: string - type: array - logVerbosity: - description: LogVerbosity overrides the default verbosity level - used to initialize loggers - format: int32 - type: integer - podResourceRequirements: - description: ResourceRequirements describes the compute resource - requirements. - properties: - claims: - description: "Claims lists the names of resources, defined - in spec.resourceClaims, that are used by this container. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of one entry in - pod.spec.resourceClaims of the Pod where this field - is used. It makes that resource available inside a - container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed - Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - preallocation: - description: Preallocation controls whether storage for DataVolumes - should be allocated in advance. - type: boolean - scratchSpaceStorageClass: - description: 'Override the storage class to used for scratch space - during transfer operations. The scratch space storage class - is determined in the following order: 1. value of scratchSpaceStorageClass, - if that doesn''t exist, use the default storage class, if there - is no default storage class, use the storage class of the DataVolume, - if no storage class specified, use no storage class for scratch - space' - type: string - tlsSecurityProfile: - description: TLSSecurityProfile is used by operators to apply - cluster-wide TLS security settings to operands. - properties: - custom: - description: "custom is a user-defined TLS security profile. - Be extremely careful using a custom profile as invalid configurations - can be catastrophic. An example custom profile looks like - this: \n ciphers: - ECDHE-ECDSA-CHACHA20-POLY1305 - ECDHE-RSA-CHACHA20-POLY1305 - - ECDHE-RSA-AES128-GCM-SHA256 - ECDHE-ECDSA-AES128-GCM-SHA256 - minTLSVersion: TLSv1.1" - nullable: true - properties: - ciphers: - description: "ciphers is used to specify the cipher algorithms - that are negotiated during the TLS handshake. Operators - may remove entries their operands do not support. For - example, to use DES-CBC3-SHA (yaml): \n ciphers: - - DES-CBC3-SHA" - items: - type: string - type: array - minTLSVersion: - description: "minTLSVersion is used to specify the minimal - version of the TLS protocol that is negotiated during - the TLS handshake. For example, to use TLS versions - 1.1, 1.2 and 1.3 (yaml): \n minTLSVersion: TLSv1.1 \n - NOTE: currently the highest minTLSVersion allowed is - VersionTLS12" - enum: - - VersionTLS10 - - VersionTLS11 - - VersionTLS12 - - VersionTLS13 - type: string - type: object - intermediate: - description: "intermediate is a TLS security profile based - on: \n https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28recommended.29 - \n and looks like this (yaml): \n ciphers: - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - TLS_CHACHA20_POLY1305_SHA256 - - ECDHE-ECDSA-AES128-GCM-SHA256 - ECDHE-RSA-AES128-GCM-SHA256 - - ECDHE-ECDSA-AES256-GCM-SHA384 - ECDHE-RSA-AES256-GCM-SHA384 - - ECDHE-ECDSA-CHACHA20-POLY1305 - ECDHE-RSA-CHACHA20-POLY1305 - - DHE-RSA-AES128-GCM-SHA256 - DHE-RSA-AES256-GCM-SHA384 - minTLSVersion: TLSv1.2" - nullable: true - type: object - modern: - description: "modern is a TLS security profile based on: \n - https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility - \n and looks like this (yaml): \n ciphers: - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - TLS_CHACHA20_POLY1305_SHA256 - minTLSVersion: TLSv1.3 \n NOTE: Currently unsupported." - nullable: true - type: object - old: - description: "old is a TLS security profile based on: \n https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility - \n and looks like this (yaml): \n ciphers: - TLS_AES_128_GCM_SHA256 - - TLS_AES_256_GCM_SHA384 - TLS_CHACHA20_POLY1305_SHA256 - - ECDHE-ECDSA-AES128-GCM-SHA256 - ECDHE-RSA-AES128-GCM-SHA256 - - ECDHE-ECDSA-AES256-GCM-SHA384 - ECDHE-RSA-AES256-GCM-SHA384 - - ECDHE-ECDSA-CHACHA20-POLY1305 - ECDHE-RSA-CHACHA20-POLY1305 - - DHE-RSA-AES128-GCM-SHA256 - DHE-RSA-AES256-GCM-SHA384 - - DHE-RSA-CHACHA20-POLY1305 - ECDHE-ECDSA-AES128-SHA256 - - ECDHE-RSA-AES128-SHA256 - ECDHE-ECDSA-AES128-SHA - ECDHE-RSA-AES128-SHA - - ECDHE-ECDSA-AES256-SHA384 - ECDHE-RSA-AES256-SHA384 - - ECDHE-ECDSA-AES256-SHA - ECDHE-RSA-AES256-SHA - DHE-RSA-AES128-SHA256 - - DHE-RSA-AES256-SHA256 - AES128-GCM-SHA256 - AES256-GCM-SHA384 - - AES128-SHA256 - AES256-SHA256 - AES128-SHA - AES256-SHA - - DES-CBC3-SHA minTLSVersion: TLSv1.0" - nullable: true - type: object - type: - description: "type is one of Old, Intermediate, Modern or - Custom. Custom provides the ability to specify individual - TLS security profile parameters. Old, Intermediate and Modern - are TLS security profiles based on: \n https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations - \n The profiles are intent based, so they may change over - time as new ciphers are developed and existing ciphers are - found to be insecure. Depending on precisely which ciphers - are available to a process, the list may be reduced. \n - Note that the Modern profile is currently not supported - because it is not yet well adopted by common software libraries." - enum: - - Old - - Intermediate - - Modern - - Custom - type: string - type: object - uploadProxyURLOverride: - description: Override the URL used when uploading to a DataVolume - type: string - type: object - customizeComponents: - description: CustomizeComponents defines patches for components deployed - by the CDI operator. - properties: - flags: - description: Configure the value used for deployment and daemonset - resources - properties: - api: - additionalProperties: - type: string - type: object - controller: - additionalProperties: - type: string - type: object - uploadProxy: - additionalProperties: - type: string - type: object - type: object - patches: - items: - description: CustomizeComponentsPatch defines a patch for some - resource. - properties: - patch: - type: string - resourceName: - minLength: 1 - type: string - resourceType: - minLength: 1 - type: string - type: - description: PatchType defines the patch type. - type: string - required: - - patch - - resourceName - - resourceType - - type - type: object - type: array - x-kubernetes-list-type: atomic - type: object - imagePullPolicy: - description: PullPolicy describes a policy for if/when to pull a container - image - enum: - - Always - - IfNotPresent - - Never - type: string - infra: - description: Selectors and tolerations that should apply to cdi infrastructure - components - properties: - affinity: - description: affinity enables pod affinity/anti-affinity placement - expanding the types of constraints that can be expressed with - nodeSelector. affinity is going to be applied to the relevant - kind of pods in parallel with nodeSelector See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for - the pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node matches the corresponding matchExpressions; - the node(s) with the highest sum are the most preferred. - items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects - (i.e. is also a no-op). - properties: - preference: - description: A node selector term, associated with - the corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the - corresponding nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from - its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: A null or empty node selector term - matches no objects. The requirements of them are - ANDed. The TopologySelectorTerm type implements - a subset of the NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - type: array - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules (e.g. - co-locate this pod in the same node, zone, etc. as some - other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to a pod label update), - the system may or may not try to eventually evict the - pod from its node. When there are multiple elements, - the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules - (e.g. avoid putting this pod in the same node, zone, etc. - as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the anti-affinity expressions - specified by this field, but it may choose a node that - violates one or more of the expressions. The node that - is most preferred is the one with the greatest sum of - weights, i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - anti-affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified - by this field are not met at scheduling time, the pod - will not be scheduled onto the node. If the anti-affinity - requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod - label update), the system may or may not try to eventually - evict the pod from its node. When there are multiple - elements, the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - type: object - apiServerReplicas: - description: ApiserverReplicas set Replicas for cdi-apiserver - format: int32 - type: integer - deploymentReplicas: - description: DeploymentReplicas set Replicas for cdi-deployment - format: int32 - type: integer - nodeSelector: - additionalProperties: - type: string - description: 'nodeSelector is the node selector applied to the - relevant kind of pods It specifies a map of key-value pairs: - for the pod to be eligible to run on a node, the node must have - each of the indicated key-value pairs as labels (it can have - additional labels as well). See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector' - type: object - tolerations: - description: tolerations is a list of tolerations applied to the - relevant kind of pods See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ - for more info. These are additional tolerations other than default - ones. - items: - description: The pod this Toleration is attached to tolerates - any taint that matches the triple using - the matching operator . - properties: - effect: - description: Effect indicates the taint effect to match. - Empty means match all taint effects. When specified, allowed - values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match - all values and all keys. - type: string - operator: - description: Operator represents a key's relationship to - the value. Valid operators are Exists and Equal. Defaults - to Equal. Exists is equivalent to wildcard for value, - so that a pod can tolerate all taints of a particular - category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of - time the toleration (which must be of effect NoExecute, - otherwise this field is ignored) tolerates the taint. - By default, it is not set, which means tolerate the taint - forever (do not evict). Zero and negative values will - be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. - type: string - type: object - type: array - uploadProxyReplicas: - description: UploadproxyReplicas set Replicas for cdi-uploadproxy - format: int32 - type: integer - type: object - priorityClass: - description: PriorityClass of the CDI control plane - type: string - uninstallStrategy: - description: CDIUninstallStrategy defines the state to leave CDI on - uninstall - enum: - - RemoveWorkloads - - BlockUninstallIfWorkloadsExist - type: string - workload: - description: Restrict on which nodes CDI workload pods will be scheduled - properties: - affinity: - description: affinity enables pod affinity/anti-affinity placement - expanding the types of constraints that can be expressed with - nodeSelector. affinity is going to be applied to the relevant - kind of pods in parallel with nodeSelector See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity - properties: - nodeAffinity: - description: Describes node affinity scheduling rules for - the pod. - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node matches the corresponding matchExpressions; - the node(s) with the highest sum are the most preferred. - items: - description: An empty preferred scheduling term matches - all objects with implicit weight 0 (i.e. it's a no-op). - A null preferred scheduling term matches no objects - (i.e. is also a no-op). - properties: - preference: - description: A node selector term, associated with - the corresponding weight. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - weight: - description: Weight associated with matching the - corresponding nodeSelectorTerm, in the range 1-100. - format: int32 - type: integer - required: - - preference - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to an update), the system - may or may not try to eventually evict the pod from - its node. - properties: - nodeSelectorTerms: - description: Required. A list of node selector terms. - The terms are ORed. - items: - description: A null or empty node selector term - matches no objects. The requirements of them are - ANDed. The TopologySelectorTerm type implements - a subset of the NodeSelectorTerm. - properties: - matchExpressions: - description: A list of node selector requirements - by node's labels. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchFields: - description: A list of node selector requirements - by node's fields. - items: - description: A node selector requirement is - a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: The label key that the selector - applies to. - type: string - operator: - description: Represents a key's relationship - to a set of values. Valid operators - are In, NotIn, Exists, DoesNotExist. - Gt, and Lt. - type: string - values: - description: An array of string values. - If the operator is In or NotIn, the - values array must be non-empty. If the - operator is Exists or DoesNotExist, - the values array must be empty. If the - operator is Gt or Lt, the values array - must have a single element, which will - be interpreted as an integer. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - x-kubernetes-map-type: atomic - type: array - required: - - nodeSelectorTerms - type: object - x-kubernetes-map-type: atomic - type: object - podAffinity: - description: Describes pod affinity scheduling rules (e.g. - co-locate this pod in the same node, zone, etc. as some - other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the affinity expressions specified - by this field, but it may choose a node that violates - one or more of the expressions. The node that is most - preferred is the one with the greatest sum of weights, - i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the affinity requirements specified by - this field are not met at scheduling time, the pod will - not be scheduled onto the node. If the affinity requirements - specified by this field cease to be met at some point - during pod execution (e.g. due to a pod label update), - the system may or may not try to eventually evict the - pod from its node. When there are multiple elements, - the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - podAntiAffinity: - description: Describes pod anti-affinity scheduling rules - (e.g. avoid putting this pod in the same node, zone, etc. - as some other pod(s)). - properties: - preferredDuringSchedulingIgnoredDuringExecution: - description: The scheduler will prefer to schedule pods - to nodes that satisfy the anti-affinity expressions - specified by this field, but it may choose a node that - violates one or more of the expressions. The node that - is most preferred is the one with the greatest sum of - weights, i.e. for each node that meets all of the scheduling - requirements (resource request, requiredDuringScheduling - anti-affinity expressions, etc.), compute a sum by iterating - through the elements of this field and adding "weight" - to the sum if the node has pods which matches the corresponding - podAffinityTerm; the node(s) with the highest sum are - the most preferred. - items: - description: The weights of all of the matched WeightedPodAffinityTerm - fields are added per-node to find the most preferred - node(s) - properties: - podAffinityTerm: - description: Required. A pod affinity term, associated - with the corresponding weight. - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by - this field and the ones listed in the namespaces - field. null selector and null or empty namespaces - list means "this pod's namespace". An empty - selector ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list - of label selector requirements. The requirements - are ANDed. - items: - description: A label selector requirement - is a selector that contains values, - a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key - that the selector applies to. - type: string - operator: - description: operator represents a - key's relationship to a set of values. - Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of - string values. If the operator is - In or NotIn, the values array must - be non-empty. If the operator is - Exists or DoesNotExist, the values - array must be empty. This array - is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator - is "In", and the values array contains - only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. - The term is applied to the union of the namespaces - listed in this field and the ones selected - by namespaceSelector. null or empty namespaces - list and null namespaceSelector means "this - pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the - pods matching the labelSelector in the specified - namespaces, where co-located is defined as - running on a node whose value of the label - with key topologyKey matches that of any node - on which any of the selected pods is running. - Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - weight: - description: weight associated with matching the - corresponding podAffinityTerm, in the range 1-100. - format: int32 - type: integer - required: - - podAffinityTerm - - weight - type: object - type: array - requiredDuringSchedulingIgnoredDuringExecution: - description: If the anti-affinity requirements specified - by this field are not met at scheduling time, the pod - will not be scheduled onto the node. If the anti-affinity - requirements specified by this field cease to be met - at some point during pod execution (e.g. due to a pod - label update), the system may or may not try to eventually - evict the pod from its node. When there are multiple - elements, the lists of nodes corresponding to each podAffinityTerm - are intersected, i.e. all terms must be satisfied. - items: - description: Defines a set of pods (namely those matching - the labelSelector relative to the given namespace(s)) - that this pod should be co-located (affinity) or not - co-located (anti-affinity) with, where co-located - is defined as running on a node whose value of the - label with key matches that of any node - on which a pod of the set of pods is running - properties: - labelSelector: - description: A label query over a set of resources, - in this case pods. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaceSelector: - description: A label query over the set of namespaces - that the term applies to. The term is applied - to the union of the namespaces selected by this - field and the ones listed in the namespaces field. - null selector and null or empty namespaces list - means "this pod's namespace". An empty selector - ({}) matches all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement - is a selector that contains values, a key, - and an operator that relates the key and - values. - properties: - key: - description: key is the label key that - the selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and - DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. - If the operator is Exists or DoesNotExist, - the values array must be empty. This - array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is - "In", and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - namespaces: - description: namespaces specifies a static list - of namespace names that the term applies to. The - term is applied to the union of the namespaces - listed in this field and the ones selected by - namespaceSelector. null or empty namespaces list - and null namespaceSelector means "this pod's namespace". - items: - type: string - type: array - topologyKey: - description: This pod should be co-located (affinity) - or not co-located (anti-affinity) with the pods - matching the labelSelector in the specified namespaces, - where co-located is defined as running on a node - whose value of the label with key topologyKey - matches that of any node on which any of the selected - pods is running. Empty topologyKey is not allowed. - type: string - required: - - topologyKey - type: object - type: array - type: object - type: object - nodeSelector: - additionalProperties: - type: string - description: 'nodeSelector is the node selector applied to the - relevant kind of pods It specifies a map of key-value pairs: - for the pod to be eligible to run on a node, the node must have - each of the indicated key-value pairs as labels (it can have - additional labels as well). See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector' - type: object - tolerations: - description: tolerations is a list of tolerations applied to the - relevant kind of pods See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ - for more info. These are additional tolerations other than default - ones. - items: - description: The pod this Toleration is attached to tolerates - any taint that matches the triple using - the matching operator . - properties: - effect: - description: Effect indicates the taint effect to match. - Empty means match all taint effects. When specified, allowed - values are NoSchedule, PreferNoSchedule and NoExecute. - type: string - key: - description: Key is the taint key that the toleration applies - to. Empty means match all taint keys. If the key is empty, - operator must be Exists; this combination means to match - all values and all keys. - type: string - operator: - description: Operator represents a key's relationship to - the value. Valid operators are Exists and Equal. Defaults - to Equal. Exists is equivalent to wildcard for value, - so that a pod can tolerate all taints of a particular - category. - type: string - tolerationSeconds: - description: TolerationSeconds represents the period of - time the toleration (which must be of effect NoExecute, - otherwise this field is ignored) tolerates the taint. - By default, it is not set, which means tolerate the taint - forever (do not evict). Zero and negative values will - be treated as 0 (evict immediately) by the system. - format: int64 - type: integer - value: - description: Value is the taint value the toleration matches - to. If the operator is Exists, the value should be empty, - otherwise just a regular string. - type: string - type: object - type: array - type: object - type: object - status: - description: CDIStatus defines the status of the installation - properties: - conditions: - description: A list of current conditions of the resource - items: - description: Condition represents the state of the operator's reconciliation - functionality. - properties: - lastHeartbeatTime: - format: date-time - type: string - lastTransitionTime: - format: date-time - type: string - message: - type: string - reason: - type: string - status: - type: string - type: - description: ConditionType is the state of the operator's reconciliation - functionality. - type: string - required: - - status - - type - type: object - type: array - observedVersion: - description: The observed version of the resource - type: string - operatorVersion: - description: The version of the resource as defined by the operator - type: string - phase: - description: Phase is the current phase of the deployment - type: string - targetVersion: - description: The desired version of the resource - type: string - type: object - required: - - spec - type: object - served: true - storage: true - subresources: {} -# IMPORTANT! Do not add status subresource, as it breaks the status update -# in CDI components. -# Delete this precaution when future CDI updates add status subresource. -# subresources: -# status: {} diff --git a/crds/embedded/datavolumes.yaml b/crds/embedded/datavolumes.yaml deleted file mode 100644 index 1d9eda8338..0000000000 --- a/crds/embedded/datavolumes.yaml +++ /dev/null @@ -1,748 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: internalvirtualizationdatavolumes.cdi.internal.virtualization.deckhouse.io - labels: - heritage: deckhouse - module: virtualization - app.kubernetes.io/component: cdi -spec: - conversion: - strategy: None - group: cdi.internal.virtualization.deckhouse.io - names: - categories: - - intvirt - kind: InternalVirtualizationDataVolume - listKind: InternalVirtualizationDataVolumeList - plural: internalvirtualizationdatavolumes - shortNames: - - intvirtdv - singular: internalvirtualizationdatavolume - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: The phase the data volume is in - jsonPath: .status.phase - name: Phase - type: string - - description: Transfer progress in percentage if known, N/A otherwise - jsonPath: .status.progress - name: Progress - type: string - - description: The number of times the transfer has been restarted. - jsonPath: .status.restartCount - name: Restarts - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1beta1 - schema: - openAPIV3Schema: - description: DataVolume is an abstraction on top of PersistentVolumeClaims - to allow easy population of those PersistentVolumeClaims with relation to - VirtualMachines - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: DataVolumeSpec defines the DataVolume type specification - properties: - checkpoints: - description: Checkpoints is a list of DataVolumeCheckpoints, representing - stages in a multistage import. - items: - description: DataVolumeCheckpoint defines a stage in a warm migration. - properties: - current: - description: Current is the identifier of the snapshot created - for this checkpoint. - type: string - previous: - description: Previous is the identifier of the snapshot from - the previous checkpoint. - type: string - required: - - current - - previous - type: object - type: array - contentType: - description: 'DataVolumeContentType options: "kubevirt", "archive"' - enum: - - kubevirt - - archive - type: string - finalCheckpoint: - description: FinalCheckpoint indicates whether the current DataVolumeCheckpoint - is the final checkpoint. - type: boolean - preallocation: - description: Preallocation controls whether storage for DataVolumes - should be allocated in advance. - type: boolean - priorityClassName: - description: PriorityClassName for Importer, Cloner and Uploader pod - type: string - pvc: - description: PVC is the PVC specification - properties: - accessModes: - description: 'accessModes contains the desired access modes the - volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' - items: - type: string - type: array - dataSource: - description: 'dataSource field can be used to specify either: - * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) If the provisioner - or an external controller can support the specified data source, - it will create a new volume based on the contents of the specified - data source. When the AnyVolumeDataSource feature gate is enabled, - dataSource contents will be copied to dataSourceRef, and dataSourceRef - contents will be copied to dataSource when dataSourceRef.namespace - is not specified. If the namespace is specified, then dataSourceRef - will not be copied to dataSource.' - properties: - apiGroup: - description: APIGroup is the group for the resource being - referenced. If APIGroup is not specified, the specified - Kind must be in the core API group. For any other third-party - types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being referenced - type: string - name: - description: Name is the name of resource being referenced - type: string - required: - - kind - - name - type: object - x-kubernetes-map-type: atomic - dataSourceRef: - description: 'dataSourceRef specifies the object from which to - populate the volume with data, if a non-empty volume is desired. - This may be any object from a non-empty API group (non core - object) or a PersistentVolumeClaim object. When this field is - specified, volume binding will only succeed if the type of the - specified object matches some installed volume populator or - dynamic provisioner. This field will replace the functionality - of the dataSource field and as such if both fields are non-empty, - they must have the same value. For backwards compatibility, - when namespace isn''t specified in dataSourceRef, both fields - (dataSource and dataSourceRef) will be set to the same value - automatically if one of them is empty and the other is non-empty. - When namespace is specified in dataSourceRef, dataSource isn''t - set to the same value and must be empty. There are three important - differences between dataSource and dataSourceRef: * While dataSource - only allows two specific types of objects, dataSourceRef allows - any non-core object, as well as PersistentVolumeClaim objects. - * While dataSource ignores disallowed values (dropping them), - dataSourceRef preserves all values, and generates an error if - a disallowed value is specified. * While dataSource only allows - local objects, dataSourceRef allows objects in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource feature - gate to be enabled. (Alpha) Using the namespace field of dataSourceRef - requires the CrossNamespaceVolumeDataSource feature gate to - be enabled.' - properties: - apiGroup: - description: APIGroup is the group for the resource being - referenced. If APIGroup is not specified, the specified - Kind must be in the core API group. For any other third-party - types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being referenced - type: string - name: - description: Name is the name of resource being referenced - type: string - namespace: - description: Namespace is the namespace of resource being - referenced Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: 'resources represents the minimum resources the volume - should have. If RecoverVolumeExpansionFailure feature is enabled - users are allowed to specify resource requirements that are - lower than previous value but must still be higher than capacity - recorded in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' - properties: - claims: - description: "Claims lists the names of resources, defined - in spec.resourceClaims, that are used by this container. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of one entry in - pod.spec.resourceClaims of the Pod where this field - is used. It makes that resource available inside a - container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed - Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - selector: - description: selector is a label query over volumes to consider - for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label selector - requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector - that contains values, a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are In, NotIn, - Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If - the operator is In or NotIn, the values array must - be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced - during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A - single {key,value} in the matchLabels map is equivalent - to an element of matchExpressions, whose key field is "key", - the operator is "In", and the values array contains only - "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - storageClassName: - description: 'storageClassName is the name of the StorageClass - required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' - type: string - volumeMode: - description: volumeMode defines what type of volume is required - by the claim. Value of Filesystem is implied when not included - in claim spec. - type: string - volumeName: - description: volumeName is the binding reference to the PersistentVolume - backing this claim. - type: string - type: object - source: - description: Source is the src of the data for the requested DataVolume - properties: - blank: - description: DataVolumeBlankImage provides the parameters to create - a new raw blank image for the PVC - type: object - gcs: - description: DataVolumeSourceGCS provides the parameters to create - a Data Volume from an GCS source - properties: - secretRef: - description: SecretRef provides the secret reference needed - to access the GCS source - type: string - url: - description: URL is the url of the GCS source - type: string - required: - - url - type: object - http: - description: DataVolumeSourceHTTP can be either an http or https - endpoint, with an optional basic auth user name and password, - and an optional configmap containing additional CAs - properties: - certConfigMap: - description: CertConfigMap is a configmap reference, containing - a Certificate Authority(CA) public key, and a base64 encoded - pem certificate - type: string - extraHeaders: - description: ExtraHeaders is a list of strings containing - extra headers to include with HTTP transfer requests - items: - type: string - type: array - secretExtraHeaders: - description: SecretExtraHeaders is a list of Secret references, - each containing an extra HTTP header that may include sensitive - information - items: - type: string - type: array - secretRef: - description: SecretRef A Secret reference, the secret should - contain accessKeyId (user name) base64 encoded, and secretKey - (password) also base64 encoded - type: string - url: - description: URL is the URL of the http(s) endpoint - type: string - required: - - url - type: object - imageio: - description: DataVolumeSourceImageIO provides the parameters to - create a Data Volume from an imageio source - properties: - certConfigMap: - description: CertConfigMap provides a reference to the CA - cert - type: string - diskId: - description: DiskID provides id of a disk to be imported - type: string - secretRef: - description: SecretRef provides the secret reference needed - to access the ovirt-engine - type: string - url: - description: URL is the URL of the ovirt-engine - type: string - required: - - diskId - - url - type: object - pvc: - description: DataVolumeSourcePVC provides the parameters to create - a Data Volume from an existing PVC - properties: - name: - description: The name of the source PVC - type: string - namespace: - description: The namespace of the source PVC - type: string - required: - - name - - namespace - type: object - registry: - description: DataVolumeSourceRegistry provides the parameters - to create a Data Volume from an registry source - properties: - certConfigMap: - description: CertConfigMap provides a reference to the Registry - certs - type: string - imageStream: - description: ImageStream is the name of image stream for import - type: string - pullMethod: - description: PullMethod can be either "pod" (default import), - or "node" (node docker cache based import) - type: string - secretRef: - description: SecretRef provides the secret reference needed - to access the Registry source - type: string - url: - description: 'URL is the url of the registry source (starting - with the scheme: docker, oci-archive)' - type: string - type: object - s3: - description: DataVolumeSourceS3 provides the parameters to create - a Data Volume from an S3 source - properties: - certConfigMap: - description: CertConfigMap is a configmap reference, containing - a Certificate Authority(CA) public key, and a base64 encoded - pem certificate - type: string - secretRef: - description: SecretRef provides the secret reference needed - to access the S3 source - type: string - url: - description: URL is the url of the S3 source - type: string - required: - - url - type: object - snapshot: - description: DataVolumeSourceSnapshot provides the parameters - to create a Data Volume from an existing VolumeSnapshot - properties: - name: - description: The name of the source VolumeSnapshot - type: string - namespace: - description: The namespace of the source VolumeSnapshot - type: string - required: - - name - - namespace - type: object - upload: - description: DataVolumeSourceUpload provides the parameters to - create a Data Volume by uploading the source - type: object - vddk: - description: DataVolumeSourceVDDK provides the parameters to create - a Data Volume from a Vmware source - properties: - backingFile: - description: BackingFile is the path to the virtual hard disk - to migrate from vCenter/ESXi - type: string - initImageURL: - description: InitImageURL is an optional URL to an image containing - an extracted VDDK library, overrides v2v-vmware config map - type: string - secretRef: - description: SecretRef provides a reference to a secret containing - the username and password needed to access the vCenter or - ESXi host - type: string - thumbprint: - description: Thumbprint is the certificate thumbprint of the - vCenter or ESXi host - type: string - url: - description: URL is the URL of the vCenter or ESXi host with - the VM to migrate - type: string - uuid: - description: UUID is the UUID of the virtual machine that - the backing file is attached to in vCenter/ESXi - type: string - type: object - type: object - sourceRef: - description: SourceRef is an indirect reference to the source of data - for the requested DataVolume - properties: - kind: - description: The kind of the source reference, currently only - "DataSource" is supported - type: string - name: - description: The name of the source reference - type: string - namespace: - description: The namespace of the source reference, defaults to - the DataVolume namespace - type: string - required: - - kind - - name - type: object - storage: - description: Storage is the requested storage specification - properties: - accessModes: - description: 'AccessModes contains the desired access modes the - volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' - items: - type: string - type: array - dataSource: - description: 'This field can be used to specify either: * An existing - VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) * An existing custom - resource that implements data population (Alpha) In order to - use custom resource types that implement data population, the - AnyVolumeDataSource feature gate must be enabled. If the provisioner - or an external controller can support the specified data source, - it will create a new volume based on the contents of the specified - data source. If the AnyVolumeDataSource feature gate is enabled, - this field will always have the same contents as the DataSourceRef - field.' - properties: - apiGroup: - description: APIGroup is the group for the resource being - referenced. If APIGroup is not specified, the specified - Kind must be in the core API group. For any other third-party - types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being referenced - type: string - name: - description: Name is the name of resource being referenced - type: string - required: - - kind - - name - type: object - x-kubernetes-map-type: atomic - dataSourceRef: - description: 'Specifies the object from which to populate the - volume with data, if a non-empty volume is desired. This may - be any local object from a non-empty API group (non core object) - or a PersistentVolumeClaim object. When this field is specified, - volume binding will only succeed if the type of the specified - object matches some installed volume populator or dynamic provisioner. - This field will replace the functionality of the DataSource - field and as such if both fields are non-empty, they must have - the same value. For backwards compatibility, both fields (DataSource - and DataSourceRef) will be set to the same value automatically - if one of them is empty and the other is non-empty. There are - two important differences between DataSource and DataSourceRef: - * While DataSource only allows two specific types of objects, - DataSourceRef allows any non-core object, as well as PersistentVolumeClaim - objects. * While DataSource ignores disallowed values (dropping - them), DataSourceRef preserves all values, and generates an - error if a disallowed value is specified. (Beta) Using this - field requires the AnyVolumeDataSource feature gate to be enabled.' - properties: - apiGroup: - description: APIGroup is the group for the resource being - referenced. If APIGroup is not specified, the specified - Kind must be in the core API group. For any other third-party - types, APIGroup is required. - type: string - kind: - description: Kind is the type of resource being referenced - type: string - name: - description: Name is the name of resource being referenced - type: string - namespace: - description: Namespace is the namespace of resource being - referenced Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: 'Resources represents the minimum resources the volume - should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' - properties: - claims: - description: "Claims lists the names of resources, defined - in spec.resourceClaims, that are used by this container. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of one entry in - pod.spec.resourceClaims of the Pod where this field - is used. It makes that resource available inside a - container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount of compute - resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. Requests cannot exceed - Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - selector: - description: A label query over volumes to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label selector - requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector - that contains values, a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are In, NotIn, - Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If - the operator is In or NotIn, the values array must - be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced - during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A - single {key,value} in the matchLabels map is equivalent - to an element of matchExpressions, whose key field is "key", - the operator is "In", and the values array contains only - "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - storageClassName: - description: 'Name of the StorageClass required by the claim. - More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' - type: string - volumeMode: - description: volumeMode defines what type of volume is required - by the claim. Value of Filesystem is implied when not included - in claim spec. - type: string - volumeName: - description: VolumeName is the binding reference to the PersistentVolume - backing this claim. - type: string - type: object - type: object - status: - description: DataVolumeStatus contains the current status of the DataVolume - properties: - claimName: - description: ClaimName is the name of the underlying PVC used by the - DataVolume. - type: string - conditions: - items: - description: DataVolumeCondition represents the state of a data - volume condition. - properties: - lastHeartbeatTime: - format: date-time - type: string - lastTransitionTime: - format: date-time - type: string - message: - type: string - reason: - type: string - status: - type: string - type: - description: DataVolumeConditionType is the string representation - of known condition types - type: string - required: - - status - - type - type: object - type: array - phase: - description: Phase is the current phase of the data volume - type: string - progress: - description: DataVolumeProgress is the current progress of the DataVolume - transfer operation. Value between 0 and 100 inclusive, N/A if not - available - type: string - restartCount: - description: RestartCount is the number of times the pod populating - the DataVolume has restarted - format: int32 - type: integer - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/crds/embedded/storageprofiles.yaml b/crds/embedded/storageprofiles.yaml new file mode 100644 index 0000000000..7adce9286e --- /dev/null +++ b/crds/embedded/storageprofiles.yaml @@ -0,0 +1,155 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + heritage: deckhouse + module: virtualization + app.kubernetes.io/component: virtualization-controller + name: internalvirtualizationstorageprofiles.cdi.internal.virtualization.deckhouse.io +spec: + group: cdi.internal.virtualization.deckhouse.io + names: + categories: + - intvirt + kind: InternalVirtualizationStorageProfile + listKind: InternalVirtualizationStorageProfileList + plural: internalvirtualizationstorageprofiles + shortNames: + - intvirtsp + singular: internalvirtualizationstorageprofile + scope: Cluster + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: StorageProfile provides storage capability recommendations + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: StorageProfileSpec defines specification for StorageProfile + properties: + claimPropertySets: + description: ClaimPropertySets is a provided set of properties applicable + to PVC + items: + description: ClaimPropertySet is a set of properties applicable + to PVC + properties: + accessModes: + description: |- + AccessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + maxItems: 4 + type: array + x-kubernetes-validations: + - message: Illegal AccessMode + rule: self.all(am, am in ['ReadWriteOnce', 'ReadOnlyMany', + 'ReadWriteMany', 'ReadWriteOncePod']) + volumeMode: + description: |- + VolumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + enum: + - Block + - Filesystem + type: string + required: + - accessModes + - volumeMode + type: object + maxItems: 8 + type: array + cloneStrategy: + description: CloneStrategy defines the preferred method for cloning + a volume + type: string + dataImportCronSourceFormat: + description: DataImportCronSourceFormat defines the format of the + DataImportCron-created disk image sources + type: string + snapshotClass: + description: SnapshotClass is optional specific VolumeSnapshotClass + for CloneStrategySnapshot. If not set, a VolumeSnapshotClass is + chosen according to the provisioner. + type: string + type: object + status: + description: StorageProfileStatus provides the most recently observed + status of the StorageProfile + properties: + claimPropertySets: + description: ClaimPropertySets computed from the spec and detected + in the system + items: + description: ClaimPropertySet is a set of properties applicable + to PVC + properties: + accessModes: + description: |- + AccessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + maxItems: 4 + type: array + x-kubernetes-validations: + - message: Illegal AccessMode + rule: self.all(am, am in ['ReadWriteOnce', 'ReadOnlyMany', + 'ReadWriteMany', 'ReadWriteOncePod']) + volumeMode: + description: |- + VolumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + enum: + - Block + - Filesystem + type: string + required: + - accessModes + - volumeMode + type: object + maxItems: 8 + type: array + cloneStrategy: + description: CloneStrategy defines the preferred method for cloning + a volume + type: string + dataImportCronSourceFormat: + description: DataImportCronSourceFormat defines the format of the + DataImportCron-created disk image sources + type: string + provisioner: + description: The Storage class provisioner plugin name + type: string + snapshotClass: + description: SnapshotClass is optional specific VolumeSnapshotClass + for CloneStrategySnapshot. If not set, a VolumeSnapshotClass is + chosen according to the provisioner. + type: string + storageClass: + description: The StorageClass name for which capabilities are defined + type: string + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/images/README.md b/images/README.md index ff6e759ef7..9ce6133595 100644 --- a/images/README.md +++ b/images/README.md @@ -4,4 +4,5 @@ Kubevirt is built as a single bundle as a virt-artifact. Then all necessary virt https://github.com/kubevirt/kubevirt/blob/v1.3.1/BUILD.bazel#L215-L224 -The same thing for cdi (cdi-artifact). +The CDI importer code used by virtual-disk-importer is vendored in +images/cdi-artifact and built from this repository. diff --git a/images/cdi-apiserver/mount-points.yaml b/images/cdi-apiserver/mount-points.yaml deleted file mode 100644 index 7f9f0c920b..0000000000 --- a/images/cdi-apiserver/mount-points.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# A list of pre-created mount points for containerd strict mode. - -dirs: - # Create dirs in /run, as /var/run is a symlink to /run. - - /run/certs/cdi-apiserver-signer-bundle - - /run/certs/cdi-apiserver-server-cert - - /kubeconfig.local diff --git a/images/cdi-apiserver/werf.inc.yaml b/images/cdi-apiserver/werf.inc.yaml deleted file mode 100644 index a005cef951..0000000000 --- a/images/cdi-apiserver/werf.inc.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -image: {{ .ModuleNamePrefix }}{{ .ImageName }} -fromImage: {{ .ModuleNamePrefix }}distroless -git: - {{- include "image mount points" . }} -import: -- image: {{ .ModuleNamePrefix }}cdi-artifact - add: /cdi-binaries - to: /usr/bin - before: setup - includePaths: - - cdi-apiserver -imageSpec: - config: - entrypoint: ["/usr/bin/cdi-apiserver", "-alsologtostderr"] - user: 64535 diff --git a/images/cdi-artifact/.gitignore b/images/cdi-artifact/.gitignore deleted file mode 100644 index c589aa74d6..0000000000 --- a/images/cdi-artifact/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore workdirs with CDI sources. -__containerized-data-importer_* diff --git a/images/cdi-artifact/LICENSE b/images/cdi-artifact/LICENSE new file mode 100644 index 0000000000..549d874d4f --- /dev/null +++ b/images/cdi-artifact/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 The KubeVirt Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/images/cdi-artifact/README.md b/images/cdi-artifact/README.md new file mode 100644 index 0000000000..8290bf23ca --- /dev/null +++ b/images/cdi-artifact/README.md @@ -0,0 +1,12 @@ +# Virtualization CDI Importer Runtime + +This directory contains the reduced importer runtime used by the Deckhouse virtualization module. +It is based on the KubeVirt Containerized Data Importer codebase and keeps the original Apache 2.0 licensing. + +Only the runtime code needed by virtualization importer pods is kept here: + +- container image unpack/import into a PVC; +- raw/qcow2 conversion, resize, and progress reporting; +- nbdkit/qemu helpers needed by that import path. + +CDI controllers, APIs, DataVolume logic, upload proxy/server, VDDK, imageio, S3, GCS, and upstream development tooling are intentionally removed. diff --git a/images/cdi-artifact/Taskfile.yaml b/images/cdi-artifact/Taskfile.yaml deleted file mode 100644 index 7c840eaa04..0000000000 --- a/images/cdi-artifact/Taskfile.yaml +++ /dev/null @@ -1,60 +0,0 @@ -version: "3" - -silent: true - -env: - CDI_REPO: "https://github.com/kubevirt/containerized-data-importer.git" - CDI_VERSION: "v1.60.3" - -tasks: - default: - cmds: - - task: status - - status: - desc: "Show git status in cloned repo" - cmds: - - | - dir=$(find . -type d -name __containerized-data-importer_\* -depth 1 | head -n1) - - if [[ -z $dir ]] ; then - echo "CDI not cloned, run 'task patch:new' to start working on a new patch." - exit 0 - fi - - cd $dir - git status - - cleanup: - desc: "Remove cloned CDI git repo" - cmds: - - | - CDI_PATH=$(find . -type d -name __containerized-data-importer_\* -depth 1) - read -p "Delete these directories? [y/N] " answer - - if [[ "${answer}" != "${answer#[Yy]}" ]] ; then - rm -rf ${CDI_PATH} - fi - - patch:new: - desc: "Checkout CDI sources, create temp branch, apply all patches as individual commits" - cmds: - - | - ../../hack/patch_helper.sh --repo ${CDI_REPO} \ - --ref ${CDI_VERSION} \ - --patches-dir ./patches - - patch:edit: - desc: "Checkout CDI sources, create temp branch, apply patches with commits, leave specified patch uncommitted" - cmds: - - | - patchName="{{.CLI_ARGS}}" - if [[ -z ${patchName} ]] ; then - echo "Specify patch name, e.g.: task patch:edit -- 001-feature-name.patch" - exit 1 - fi - - ../../hack/patch_helper.sh --repo ${CDI_REPO} \ - --ref ${CDI_VERSION} \ - --patches-dir ./patches \ - --stop-at ${patchName} diff --git a/images/cdi-artifact/cmd/cdi-importer/importer.go b/images/cdi-artifact/cmd/cdi-importer/importer.go new file mode 100644 index 0000000000..d0a9c82b5c --- /dev/null +++ b/images/cdi-artifact/cmd/cdi-importer/importer.go @@ -0,0 +1,216 @@ +package main + +// importer.go imports a registry image into a target PVC. +// This process expects several environmental variables: +// ImporterEndpoint Source registry image URL. +// ImporterAccessKeyID Optional. Access key is the user ID that uniquely identifies your +// account. +// ImporterSecretKey Optional. Secret key is the password to your account. + +import ( + "errors" + "flag" + "fmt" + "os" + "strconv" + + v1 "k8s.io/api/core/v1" + "k8s.io/klog/v2" + "k8s.io/utils/ptr" + + "kubevirt.io/containerized-data-importer/pkg/common" + "kubevirt.io/containerized-data-importer/pkg/importer" + "kubevirt.io/containerized-data-importer/pkg/util" + prometheusutil "kubevirt.io/containerized-data-importer/pkg/util/prometheus" +) + +const ( + completeMessage = "Import Complete" + + sourceRegistry = "registry" + + contentTypeKubeVirt = "kubevirt" + contentTypeArchive = "archive" +) + +func init() { + klog.InitFlags(nil) + flag.Parse() +} + +func touchDoneFile() { + doneFile, _ := util.ParseEnvVar(common.ImporterDoneFile, false) + if doneFile == "" { + return + } + f, err := os.OpenFile(doneFile, os.O_CREATE|os.O_EXCL, 0666) + if err != nil { + klog.Errorf("Failed creating file %s: %+v", doneFile, err) + } + f.Close() +} + +func main() { + defer klog.Flush() + + certsDirectory, err := os.MkdirTemp("", "certsdir") + if err != nil { + panic(err) + } + defer os.RemoveAll(certsDirectory) + prometheusutil.StartPrometheusEndpoint(certsDirectory) + klog.V(1).Infoln("Starting importer") + + source, _ := util.ParseEnvVar(common.ImporterSource, false) + contentType, _ := util.ParseEnvVar(common.ImporterContentType, false) + imageSize, _ := util.ParseEnvVar(common.ImporterImageSize, false) + filesystemOverhead, _ := strconv.ParseFloat(os.Getenv(common.FilesystemOverheadVar), 64) + preallocation, err := strconv.ParseBool(os.Getenv(common.Preallocation)) + if err != nil { + klog.Errorf(`the %s environment variable is with a wrong value "%s"; should be "true" or "false"`, common.Preallocation, os.Getenv(common.Preallocation)) + os.Exit(1) + } + + volumeMode := v1.PersistentVolumeBlock + if _, err := os.Stat(common.WriteBlockPath); os.IsNotExist(err) { + volumeMode = v1.PersistentVolumeFilesystem + } + + // With writeback cache mode it's possible that the process will exit before all writes have been committed to storage. + // To guarantee that our write was committed to storage, we make a fsync syscall and ensure success. + // Also might be a good idea to sync any chmod's we might have done. + defer fsyncDataFile(contentType, volumeMode) + + //Registry import currently support kubevirt content type only + if contentType != contentTypeKubeVirt && source == sourceRegistry { + klog.Errorf("Unsupported content type %s when importing from %s", contentType, source) + os.Exit(1) + } + + if _, err := util.GetAvailableSpaceByVolumeMode(volumeMode); err != nil { + klog.Errorf("%+v", err) + os.Exit(1) + } + + exitCode := handleImport(source, contentType, volumeMode, imageSize, filesystemOverhead, preallocation) + if exitCode != 0 { + os.Exit(exitCode) + } +} + +func handleImport( + source string, + contentType string, + volumeMode v1.PersistentVolumeMode, + imageSize string, + filesystemOverhead float64, + preallocation bool) int { + klog.V(1).Infoln("begin import process") + + ds := newDataSource(source) + defer ds.Close() + + processor := newDataProcessor(contentType, volumeMode, ds, imageSize, filesystemOverhead, preallocation) + err := processor.ProcessData() + + scratchSpaceRequired := errors.Is(err, importer.ErrRequiresScratchSpace) + if err != nil && !scratchSpaceRequired { + klog.Errorf("%+v", err) + if err := util.WriteTerminationMessage(fmt.Sprintf("Unable to process data: %v", err.Error())); err != nil { + klog.Errorf("%+v", err) + } + return 1 + } + + termMsg := ds.GetTerminationMessage() + if termMsg == nil { + termMsg = &common.TerminationMessage{} + } + termMsg.ScratchSpaceRequired = &scratchSpaceRequired + termMsg.PreallocationApplied = ptr.To(processor.PreallocationApplied()) + termMsg.Message = ptr.To(completeMessage) + + touchDoneFile() + if err := writeTerminationMessage(termMsg); err != nil { + klog.Errorf("%+v", err) + return 1 + } + + if scratchSpaceRequired { + // Exiting instead of returning 0 as normally to avoid clashing + // with cleanup functions (fsyncDataFile) that assume the imported + // file will be there during regular exit. + os.Exit(0) + } + + return 0 +} + +func writeTerminationMessage(termMsg *common.TerminationMessage) error { + msg, err := termMsg.String() + if err != nil { + return err + } + if err := util.WriteTerminationMessage(msg); err != nil { + return err + } + klog.V(1).Infoln(msg) + return nil +} + +func newDataProcessor(contentType string, volumeMode v1.PersistentVolumeMode, ds importer.DataSourceInterface, imageSize string, filesystemOverhead float64, preallocation bool) *importer.DataProcessor { + dest := getImporterDestPath(contentType, volumeMode) + processor := importer.NewDataProcessor(ds, dest, common.ImporterDataDir, common.ScratchDataDir, imageSize, filesystemOverhead, preallocation, os.Getenv(common.CacheMode)) + return processor +} + +func getImporterDestPath(contentType string, volumeMode v1.PersistentVolumeMode) string { + dest := common.ImporterWritePath + + if contentType == contentTypeArchive { + dest = common.ImporterVolumePath + } + if volumeMode == v1.PersistentVolumeBlock { + dest = common.WriteBlockPath + } + + return dest +} + +func newDataSource(source string) importer.DataSourceInterface { + ep, _ := util.ParseEnvVar(common.ImporterEndpoint, false) + acc, _ := util.ParseEnvVar(common.ImporterAccessKeyID, false) + sec, _ := util.ParseEnvVar(common.ImporterSecretKey, false) + certDir, _ := util.ParseEnvVar(common.ImporterCertDirVar, false) + insecureTLS, _ := strconv.ParseBool(os.Getenv(common.InsecureTLSVar)) + + switch source { + case sourceRegistry: + ds := importer.NewRegistryDataSource(ep, acc, sec, certDir, insecureTLS) + return ds + default: + klog.Errorf("Unknown source type %s\n", source) + err := util.WriteTerminationMessage(fmt.Sprintf("Unknown data source: %s", source)) + if err != nil { + klog.Errorf("%+v", err) + } + os.Exit(1) + } + + return nil +} + +func fsyncDataFile(contentType string, volumeMode v1.PersistentVolumeMode) { + dataFile := getImporterDestPath(contentType, volumeMode) + file, err := os.Open(dataFile) + if err != nil { + klog.Errorf("could not get file descriptor for fsync call: %+v", err) + os.Exit(1) + } + if err := file.Sync(); err != nil { + klog.Errorf("could not fsync following qemu-img writing: %+v", err) + os.Exit(1) + } + klog.V(3).Infof("Successfully completed fsync(%s) syscall, committed to disk\n", dataFile) + file.Close() +} diff --git a/images/cdi-artifact/go.mod b/images/cdi-artifact/go.mod new file mode 100644 index 0000000000..e2dff67e4a --- /dev/null +++ b/images/cdi-artifact/go.mod @@ -0,0 +1,117 @@ +module kubevirt.io/containerized-data-importer + +go 1.24.0 + +toolchain go1.24.8 + +require ( + github.com/containers/image/v5 v5.32.0 + github.com/docker/go-units v0.5.0 + github.com/klauspost/compress v1.17.9 + github.com/machadovilaca/operator-observability v0.0.20 + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/client_model v0.6.0 + github.com/ulikunitz/xz v0.5.15 + golang.org/x/sys v0.39.0 + k8s.io/api v0.30.2 + k8s.io/apimachinery v0.30.2 + k8s.io/client-go v8.0.0+incompatible + k8s.io/klog/v2 v2.120.1 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b +) + +require ( + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect + github.com/containers/ocicrypt v1.2.0 // indirect + github.com/containers/storage v1.55.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/user v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/onsi/ginkgo/v2 v2.17.1 // indirect + github.com/onsi/gomega v1.33.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/prometheus/common v0.51.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/vbatts/tar-split v0.11.5 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace ( + github.com/chzyer/logex => github.com/chzyer/logex v1.2.1 + github.com/openshift/api => github.com/openshift/api v0.0.0-20240625084701-0689f006bcde + github.com/openshift/client-go => github.com/openshift/client-go v0.0.0-20240528061634-b054aa794d87 + github.com/openshift/library-go => github.com/openshift/library-go v0.0.0-20240621150525-4bb4238aef81 + github.com/operator-framework/operator-lifecycle-manager => github.com/operator-framework/operator-lifecycle-manager v0.0.0-20190128024246-5eb7ae5bdb7a + + k8s.io/api => k8s.io/api v0.30.2 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.30.2 + k8s.io/apimachinery => k8s.io/apimachinery v0.30.2 + k8s.io/apiserver => k8s.io/apiserver v0.30.2 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.30.2 + k8s.io/client-go => k8s.io/client-go v0.30.2 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.30.2 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.30.2 + k8s.io/code-generator => k8s.io/code-generator v0.30.2 + k8s.io/component-base => k8s.io/component-base v0.30.2 + k8s.io/component-helpers => k8s.io/component-helpers v0.30.2 + k8s.io/controller-manager => k8s.io/controller-manager v0.30.2 + k8s.io/cri-api => k8s.io/cri-api v0.30.2 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.30.2 + k8s.io/dynamic-resource-allocation => dynamic-resource-allocation v0.30.2 + k8s.io/endpointslice => k8s.io/endpointslice v0.30.2 + k8s.io/kms => k8s.io/kms v0.30.2 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.30.2 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.30.2 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.30.2 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.30.2 + k8s.io/kubectl => k8s.io/kubectl v0.30.2 + k8s.io/kubelet => k8s.io/kubelet v0.30.2 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.30.2 + k8s.io/metrics => k8s.io/metrics v0.30.2 + k8s.io/mount-utils => k8s.io/mount-utils v0.30.2 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.30.2 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.30.2 + k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.30.2 + k8s.io/sample-controller => k8s.io/sample-controller v0.30.2 + + kubevirt.io/controller-lifecycle-operator-sdk/api => kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 + sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.18.4 +) + +replace golang.org/x/crypto => golang.org/x/crypto v0.45.0 // CVE-2024-45337,CVE-2025-22869,CVE-2025-47914 + +// Use reduced github.com/docker/docker module: only compare is needed for CDI. +replace github.com/docker/docker => ./staging/src/github.com/docker/docker // CVE-2026-34040,CVE-2026-33997,CVE-2026-41567,CVE-2026-42306,CVE-2026-41568 diff --git a/images/cdi-artifact/go.sum b/images/cdi-artifact/go.sum new file mode 100644 index 0000000000..d22dfa1081 --- /dev/null +++ b/images/cdi-artifact/go.sum @@ -0,0 +1,258 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containers/image/v5 v5.32.0 h1:yjbweazPfr8xOzQ2hkkYm1A2V0jN96/kES6Gwyxj7hQ= +github.com/containers/image/v5 v5.32.0/go.mod h1:x5e0RDfGaY6bnQ13gJ2LqbfHvzssfB/y5a8HduGFxJc= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/ocicrypt v1.2.0 h1:X14EgRK3xNFvJEfI5O4Qn4T3E25ANudSOZz/sirVuPM= +github.com/containers/ocicrypt v1.2.0/go.mod h1:ZNviigQajtdlxIZGibvblVuIFBKIuUI2M0QM12SD31U= +github.com/containers/storage v1.55.0 h1:wTWZ3YpcQf1F+dSP4KxG9iqDfpQY1otaUXjPpffuhgg= +github.com/containers/storage v1.55.0/go.mod h1:28cB81IDk+y7ok60Of6u52RbCeBRucbFOeLunhER1RQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= +github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= +github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/machadovilaca/operator-observability v0.0.20 h1:I9CLKWcaJU9KtPREhUu4yn/CLAZUpxFqEUz/ZVenkAI= +github.com/machadovilaca/operator-observability v0.0.20/go.mod h1:e4Z3VhOXb9InkmSh00JjqBBijE+iD+YMzynBpKB3+gE= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/user v0.2.0 h1:OnpapJsRp25vkhw8TFG6OLJODNh/3rEwRWtJ3kakwRM= +github.com/moby/sys/user v0.2.0/go.mod h1:RYstrcWOJpVh+6qzUqp2bU3eaRpdiQeKGlKitaH0PM8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= +github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw= +github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= +github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= +k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/images/cdi-artifact/pkg/common/common.go b/images/cdi-artifact/pkg/common/common.go new file mode 100644 index 0000000000..d5251522c7 --- /dev/null +++ b/images/cdi-artifact/pkg/common/common.go @@ -0,0 +1,67 @@ +package common + +import ( + "encoding/json" + "fmt" +) + +const ( + ImporterVolumePath = "/data" + DiskImageName = "disk.img" + ImporterWritePath = ImporterVolumePath + "/" + DiskImageName + WriteBlockPath = "/dev/cdi-block-volume" + ImporterDataDir = "/data" + ScratchDataDir = "/scratch" + NbdkitLogPath = "/tmp/nbdkit.log" + + PodTerminationMessageFile = "/dev/termination-log" + + ImporterSource = "IMPORTER_SOURCE" + ImporterContentType = "IMPORTER_CONTENTTYPE" + ImporterEndpoint = "IMPORTER_ENDPOINT" + ImporterAccessKeyID = "IMPORTER_ACCESS_KEY_ID" + ImporterSecretKey = "IMPORTER_SECRET_KEY" + ImporterImageSize = "IMPORTER_IMAGE_SIZE" + ImporterCertDirVar = "IMPORTER_CERT_DIR" + ImporterDoneFile = "IMPORTER_DONE_FILE" + ImporterProxyCertDir = "/proxycerts/" + InsecureTLSVar = "INSECURE_TLS" + CacheMode = "CACHE_MODE" + CacheModeTryNone = "TRYNONE" + Preallocation = "PREALLOCATION" + FilesystemOverheadVar = "FILESYSTEM_OVERHEAD" + OwnerUID = "OWNER_UID" + + GenericError = "Error" + PreallocationApplied = "Preallocation applied" + ScratchSpaceRequired = "scratch space required and none found" + ImagePullFailureText = "failed to pull image" +) + +// TerminationMessage contains data to be serialized and used as the termination message of the importer. +type TerminationMessage struct { + ScratchSpaceRequired *bool `json:"scratchSpaceRequired,omitempty"` + PreallocationApplied *bool `json:"preallocationApplied,omitempty"` + DeadlinePassed *bool `json:"deadlinePassed,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Message *string `json:"message,omitempty"` +} + +func (it *TerminationMessage) String() (string, error) { + msg, err := json.Marshal(it) + if err != nil { + return "", err + } + + // Messages longer than 4096 are truncated by kubelet. + if length := len(msg); length > 4096 { + return "", fmt.Errorf("termination message length %d exceeds maximum length of 4096 bytes", length) + } + + return string(msg), nil +} + +// ServerInfo contains data to be serialized and used as the body of responses to the container image server info endpoint. +type ServerInfo struct { + Env []string `json:"env,omitempty"` +} diff --git a/images/cdi-artifact/pkg/image/directio.go b/images/cdi-artifact/pkg/image/directio.go new file mode 100644 index 0000000000..f3657d510a --- /dev/null +++ b/images/cdi-artifact/pkg/image/directio.go @@ -0,0 +1,116 @@ +/* +Copyright 2023 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package image + +import ( + "io" + "os" + "syscall" + + "github.com/pkg/errors" + + "k8s.io/klog/v2" +) + +// OSInterface collects system level operations that need to be mocked out +// during tests. +type OSInterface interface { + Stat(path string) (os.FileInfo, error) + Remove(path string) error + OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) +} + +// RealOS is used to dispatch the real system level operations. +type RealOS struct{} + +// Stat will call os.Stat to get the FileInfo for a given path +func (RealOS) Stat(path string) (os.FileInfo, error) { + return os.Stat(path) +} + +// Remove will call os.Remove to remove the path. +func (RealOS) Remove(path string) error { + return os.Remove(path) +} + +// OpenFile will call os.OpenFile to return the file. +func (RealOS) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { + return os.OpenFile(name, flag, perm) +} + +// DirectIOChecker checks if a certain destination supports direct I/O (bypassing page cache) +type DirectIOChecker interface { + CheckBlockDevice(path string) (bool, error) + CheckFile(path string) (bool, error) +} + +type directIOChecker struct { + OSInterface OSInterface +} + +// NewDirectIOChecker returns a new direct I/O checker +func NewDirectIOChecker(osInterface OSInterface) DirectIOChecker { + return &directIOChecker{ + OSInterface: osInterface, + } +} + +func (c *directIOChecker) CheckBlockDevice(path string) (bool, error) { + return c.check(path, syscall.O_RDONLY) +} + +func (c *directIOChecker) CheckFile(path string) (bool, error) { + flags := syscall.O_RDONLY + if _, err := c.OSInterface.Stat(path); errors.Is(err, os.ErrNotExist) { + // try to create the file and perform the check + flags = flags | syscall.O_CREAT + defer removeTempFileAndCheckErr(c.OSInterface, path) + } + return c.check(path, flags) +} + +// based on https://github.com/kubevirt/kubevirt/blob/c4fc4ab72a868399f5331438f35b8c33e7dd0720/pkg/virt-launcher/virtwrap/converter/converter.go#L346 +func (c *directIOChecker) check(path string, flags int) (bool, error) { + // #nosec No risk for path injection as we only open the file, not read from it. The function leaks only whether the directory to `path` exists. + f, err := c.OSInterface.OpenFile(path, flags|syscall.O_DIRECT, 0600) + if err != nil { + // EINVAL is returned if the filesystem does not support the O_DIRECT flag + if perr := (&os.PathError{}); errors.As(err, &perr) && errors.Is(perr, syscall.EINVAL) { + // #nosec No risk for path injection as we only open the file, not read from it. The function leaks only whether the directory to `path` exists. + f, err := c.OSInterface.OpenFile(path, flags & ^syscall.O_DIRECT, 0600) + if err == nil { + defer closeIOAndCheckErr(f) + return false, nil + } + } + return false, err + } + defer closeIOAndCheckErr(f) + return true, nil +} + +func closeIOAndCheckErr(c io.Closer) { + if ferr := c.Close(); ferr != nil { + klog.Errorf("Error when closing file: \n%s\n", ferr) + } +} + +func removeTempFileAndCheckErr(osInterface OSInterface, path string) { + if ferr := osInterface.Remove(path); ferr != nil { + klog.Errorf("Error when removing file: \n%s\n", ferr) + } +} diff --git a/images/cdi-artifact/pkg/image/filefmt.go b/images/cdi-artifact/pkg/image/filefmt.go new file mode 100644 index 0000000000..979fbb33c6 --- /dev/null +++ b/images/cdi-artifact/pkg/image/filefmt.go @@ -0,0 +1,120 @@ +package image + +import ( + "bytes" + "encoding/hex" + "strconv" + + "github.com/pkg/errors" + + "k8s.io/klog/v2" +) + +// MaxExpectedHdrSize defines the Size of buffer used to read file headers. +// Note: this is the size of tar's header. If a larger number is used the tar unarchive operation +// +// creates the destination file too large, by the difference between this const and 512. +const MaxExpectedHdrSize = 512 + +// Headers provides a map for header info, key is file format, eg. "gz" or "tar", value is metadata describing the layout for this hdr +type Headers map[string]Header + +var knownHeaders = Headers{ + "gz": Header{ + Format: "gz", + magicNumber: []byte{0x1F, 0x8B}, + // TODO: size not in hdr + SizeOff: 0, + SizeLen: 0, + }, + "zst": Header{ + Format: "zst", + magicNumber: []byte{0x28, 0xb5, 0x2f, 0xfd}, + SizeOff: 0, + SizeLen: 0, + }, + "qcow2": Header{ + Format: "qcow2", + magicNumber: []byte{'Q', 'F', 'I', 0xfb}, + mgOffset: 0, + SizeOff: 24, + SizeLen: 8, + }, + "tar": Header{ + Format: "tar", + magicNumber: []byte{0x75, 0x73, 0x74, 0x61, 0x72}, + mgOffset: 0x101, + SizeOff: 124, + SizeLen: 8, + }, + "xz": Header{ + Format: "xz", + magicNumber: []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, + // TODO: size not in hdr + SizeOff: 0, + SizeLen: 0, + }, + "vmdk": Header{ + Format: "vmdk", + magicNumber: []byte("KDMV"), + SizeOff: 0, + SizeLen: 0, + }, + "vdi": Header{ + Format: "vdi", + magicNumber: []byte{0x7F, 0x10, 0xDA, 0xBE}, + mgOffset: 0x40, + SizeOff: 0, + SizeLen: 0, + }, + "vhd": Header{ + Format: "vhd", + magicNumber: []byte("conectix"), + SizeOff: 0, + SizeLen: 0, + }, + "vhdx": Header{ + Format: "vhdx", + magicNumber: []byte("vhdxfile"), + SizeOff: 0, + SizeLen: 0, + }, +} + +// Header represents our parameters for a file format header +type Header struct { + Format string + magicNumber []byte + mgOffset int + SizeOff int // in bytes + SizeLen int // in bytes +} + +// CopyKnownHdrs performs a simple map copy since := assignment copies the reference to the map, not contents. +func CopyKnownHdrs() Headers { + m := make(Headers) + for k, v := range knownHeaders { + m[k] = v + } + return m +} + +// Match performs a check to see if the provided byte slice matches the bytes in our header data +func (h Header) Match(b []byte) bool { + return bytes.Equal(b[h.mgOffset:h.mgOffset+len(h.magicNumber)], h.magicNumber) +} + +// Size uses the Header receiver offset and length fields to extract, from the passed-in file header slice (b), +// the size of the original file. It is not guaranteed that the header is known to cdi and thus 0 may be returned as the size. +func (h Header) Size(b []byte) (int64, error) { + if h.SizeLen == 0 { // no size is supported in this format's header + return 0, nil + } + s := hex.EncodeToString(b[h.SizeOff : h.SizeOff+h.SizeLen]) + size, err := strconv.ParseInt(s, 16, 64) + if err != nil { + return 0, errors.Wrapf(err, "unable to determine original file size from %+v", s) + } + klog.V(3).Infof("Size: %q size in bytes (at off %d:%d): %d", h.Format, h.SizeOff, h.SizeOff+h.SizeLen, size) + return size, nil +} diff --git a/images/cdi-artifact/pkg/image/nbdkit.go b/images/cdi-artifact/pkg/image/nbdkit.go new file mode 100644 index 0000000000..9667a7163f --- /dev/null +++ b/images/cdi-artifact/pkg/image/nbdkit.go @@ -0,0 +1,324 @@ +package image + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "github.com/pkg/errors" + + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" +) + +const ( + startupTimeoutSeconds = 15 + defaultUserAgent = "cdi-nbdkit-importer" +) + +// NbdkitPlugin represents a plugin for nbdkit +type NbdkitPlugin string + +// NbdkitFilter represents s filter for nbdkit +type NbdkitFilter string + +// NbdkitLogWatcher allows custom handling of nbdkit log messages +type NbdkitLogWatcher interface { + Start(*bufio.Reader) + Stop() +} + +// Nbdkit plugins +const ( + NbdkitCurlPlugin NbdkitPlugin = "curl" +) + +// Nbdkit filters +const ( + NbdkitRetryFilter NbdkitFilter = "retry" + NbdkitReadAheadFilter NbdkitFilter = "readahead" +) + +// Nbdkit represents struct for an nbdkit instance +type Nbdkit struct { + c *exec.Cmd + NbdPidFile string + nbdkitArgs []string + plugin NbdkitPlugin + pluginArgs []string + redactArgs []string + filters []NbdkitFilter + Socket string + Env []string + LogWatcher NbdkitLogWatcher +} + +// NbdkitOperation defines the interface for executing nbdkit +type NbdkitOperation interface { + StartNbdkit(source string) error + KillNbdkit() error + AddEnvVariable(v string) + AddFilter(filter NbdkitFilter) +} + +// NewNbdkitCurl creates a new Nbdkit instance with the curl plugin +func NewNbdkitCurl(nbdkitPidFile, user, password, certDir, socket string, extraHeaders, secretExtraHeaders []string) (NbdkitOperation, error) { + var pluginArgs []string + var redactArgs []string + args := []string{"-r"} + pluginArgs = append(pluginArgs, fmt.Sprintf("header=User-Agent: %s", defaultUserAgent)) + if user != "" { + pluginArgs = append(pluginArgs, "user="+user) + } + if password != "" { + passwordfile, err := writePasswordFile(password) + if err != nil { + return nil, err + } + pluginArgs = append(pluginArgs, "password=+"+passwordfile) + } + if certDir != "" { + pluginArgs = append(pluginArgs, fmt.Sprintf("cainfo=%s/%s", certDir, "tls.crt")) + } + for _, header := range extraHeaders { + pluginArgs = append(pluginArgs, fmt.Sprintf("header=%s", header)) + } + // Don't do exponential retry, the container restart will be exponential + pluginArgs = append(pluginArgs, "retry-exponential=no") + for _, header := range secretExtraHeaders { + redactArgs = append(redactArgs, fmt.Sprintf("header=%s", header)) + } + n := &Nbdkit{ + NbdPidFile: nbdkitPidFile, + plugin: NbdkitCurlPlugin, + nbdkitArgs: args, + pluginArgs: pluginArgs, + redactArgs: redactArgs, + Socket: socket, + } + // QEMU generally reads through the extents in order, making the readahead + // filter quite appropriate. + n.AddFilter(NbdkitReadAheadFilter) + // Should be last filter + n.AddFilter(NbdkitRetryFilter) + return n, nil +} + +func writePasswordFile(password string) (string, error) { + f, err := os.CreateTemp("", "password") + if err != nil { + return "", err + } + defer f.Close() + // This would be nice but we don't want to delete it until + // program exit. + // defer os.Remove(f.Name()) + + if _, err := f.Write([]byte(password)); err != nil { + return "", err + } + if err := f.Close(); err != nil { + return "", err + } + return f.Name(), nil +} + +// AddEnvVariable adds an environmental variable to the nbdkit command +func (n *Nbdkit) AddEnvVariable(v string) { + env := os.Environ() + env = append(env, v) + n.Env = env +} + +// AddFilter adds a nbdkit filter if it doesn't already exist +func (n *Nbdkit) AddFilter(filter NbdkitFilter) { + for _, f := range n.filters { + if f == filter { + return + } + } + n.filters = append(n.filters, filter) +} + +func (n *Nbdkit) getSourceArg(s string) string { + var source string + switch n.plugin { + case NbdkitCurlPlugin: + source = fmt.Sprintf("url=%s", s) + default: + source = s + } + return source +} + +// StartNbdkit starts nbdkit process +func (n *Nbdkit) StartNbdkit(source string) error { + var err error + argsNbdkit := []string{ + "--foreground", + "--readonly", + "--exit-with-parent", + "-U", n.Socket, + "--pidfile", n.NbdPidFile, + } + // set filters + for _, f := range n.filters { + argsNbdkit = append(argsNbdkit, fmt.Sprintf("--filter=%s", f)) + } + // set additional arguments + argsNbdkit = append(argsNbdkit, n.nbdkitArgs...) + + // append nbdkit plugin arguments + argsNbdkit = append(argsNbdkit, string(n.plugin)) + argsNbdkit = append(argsNbdkit, n.pluginArgs...) + argsNbdkit = append(argsNbdkit, n.redactArgs...) + argsNbdkit = append(argsNbdkit, n.getSourceArg(source)) + + isRedacted := func(arg string) bool { + for _, value := range n.redactArgs { + if value == arg { + return true + } + } + return false + } + + quotedArgs := make([]string, len(argsNbdkit)) + for index, value := range argsNbdkit { + if isRedacted(value) { + if strings.HasPrefix(value, "header=") { + quotedArgs[index] = "'header=/secret redacted/'" + } else { + quotedArgs[index] = "'/secret redacted/'" + } + } else { + quotedArgs[index] = "'" + value + "'" + } + } + klog.V(3).Infof("Start nbdkit with: %v", quotedArgs) + + n.c = exec.Command("nbdkit", argsNbdkit...) + var stdout io.ReadCloser + stdout, err = n.c.StdoutPipe() + if err != nil { + klog.Errorf("Error constructing stdout pipe: %v", err) + return err + } + n.c.Stderr = n.c.Stdout + output := bufio.NewReader(stdout) + if n.LogWatcher != nil { + n.LogWatcher.Start(output) + } else { + go watchNbdLog(output) + } + + err = n.c.Start() + if err != nil { + klog.Errorf("Unable to start nbdkit: %v", err) + return err + } + + err = waitForNbd(n.NbdPidFile) + if err != nil { + klog.Errorf("Failed waiting for nbdkit to start up: %v", err) + return err + } + return nil +} + +// Default nbdkit log watcher, logs lines as nbdkit prints them, +// and appends them to the nbdkit log file. +func watchNbdLog(output *bufio.Reader) { + f, err := os.Create(common.NbdkitLogPath) + if err != nil { + klog.Errorf("Error writing nbdkit log to file: %v", err) + } + defer f.Close() + + scanner := bufio.NewScanner(output) + for scanner.Scan() { + line := scanner.Text() + logLine := fmt.Sprintf("Log line from nbdkit: %s", line) + klog.Info(logLine) + _, err = f.WriteString(logLine) + if err != nil { + klog.Errorf("failed to write log line; %v", err) + } + } + if err := scanner.Err(); err != nil { + klog.Errorf("Error watching nbdkit log: %v", err) + } + klog.Infof("Stopped watching nbdkit log.") +} + +// waitForNbd waits for nbdkit to start by watching for the existence of the given PID file. +func waitForNbd(pidfile string) error { + nbdCheck := make(chan bool, 1) + go func() { + klog.Infoln("Waiting for nbdkit PID.") + for { + select { + case <-nbdCheck: + return + case <-time.After(500 * time.Millisecond): + _, err := os.Stat(pidfile) + if err != nil { + if !os.IsNotExist(err) { + klog.Warningf("Error checking for nbdkit PID: %v", err) + } + } else { + nbdCheck <- true + return + } + } + } + }() + + select { + case <-nbdCheck: + klog.Infoln("nbdkit ready.") + return nil + case <-time.After(startupTimeoutSeconds * time.Second): + nbdCheck <- true + return errors.New("timed out waiting for nbdkit to be ready") + } +} + +// KillNbdkit stops the nbdkit process +func (n *Nbdkit) KillNbdkit() error { + var err error + if n.c == nil { + return nil + } + if n.c.Process != nil { + err = n.c.Process.Signal(os.Interrupt) + if err != nil { + err = n.c.Process.Kill() + } + } + if n.LogWatcher != nil { + n.LogWatcher.Stop() + } + return err +} + +type mockNbdkit struct{} + +// NewMockNbdkitCurl creates a mock nbdkit curl plugin for testing +func NewMockNbdkitCurl(nbdkitPidFile, user, password, certDir, socket string, extraHeaders, secretExtraHeaders []string) (NbdkitOperation, error) { + return &mockNbdkit{}, nil +} + +func (m *mockNbdkit) StartNbdkit(source string) error { + return nil +} +func (m *mockNbdkit) KillNbdkit() error { + return nil +} +func (m *mockNbdkit) AddEnvVariable(v string) {} +func (m *mockNbdkit) AddFilter(filter NbdkitFilter) {} diff --git a/images/cdi-artifact/pkg/image/qemu.go b/images/cdi-artifact/pkg/image/qemu.go new file mode 100644 index 0000000000..d79585e167 --- /dev/null +++ b/images/cdi-artifact/pkg/image/qemu.go @@ -0,0 +1,404 @@ +/* +Copyright 2018 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package image + +import ( + "encoding/json" + "fmt" + "net/url" + "os" + "regexp" + "strconv" + "strings" + + "github.com/docker/go-units" + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" + metrics "kubevirt.io/containerized-data-importer/pkg/monitoring/metrics/cdi-importer" + "kubevirt.io/containerized-data-importer/pkg/system" + "kubevirt.io/containerized-data-importer/pkg/util" +) + +const ( + networkTimeoutSecs = 3600 //max is 10000 + maxMemory = 1 << 30 //value from OpenStack Nova + maxCPUSecs = 30 //value from OpenStack Nova + matcherString = "\\((\\d?\\d\\.\\d\\d)\\/100%\\)" +) + +// ImgInfo contains the virtual image information. +type ImgInfo struct { + // Format contains the format of the image + Format string `json:"format"` + // BackingFile is the file name of the backing file + BackingFile string `json:"backing-filename"` + // VirtualSize is the disk size of the image which will be read by vm + VirtualSize int64 `json:"virtual-size"` + // ActualSize is the size of the qcow2 image + ActualSize int64 `json:"actual-size"` +} + +// QEMUOperations defines the interface for executing qemu subprocesses +type QEMUOperations interface { + ConvertToRawStream(*url.URL, string, bool, string) error + ConvertToFormatStream(url *url.URL, format, dest string, preallocate bool) error + Resize(string, resource.Quantity, bool) error + Info(url *url.URL) (*ImgInfo, error) + Validate(*url.URL, int64) error + CreateBlankImage(string, resource.Quantity, bool) error + Rebase(backingFile string, delta string) error + Commit(image string) error +} + +type qemuOperations struct{} + +var ( + qemuExecFunction = system.ExecWithLimits + qemuInfoLimits = &system.ProcessLimitValues{AddressSpaceLimit: maxMemory, CPUTimeLimit: maxCPUSecs} + qemuIterface = NewQEMUOperations() + re = regexp.MustCompile(matcherString) + + ownerUID string + convertPreallocationMethods = [][]string{ + {"-o", "preallocation=falloc"}, + {"-o", "preallocation=full"}, + {"-S", "0"}, + } + resizePreallocationMethods = [][]string{ + {"--preallocation=falloc"}, + {"--preallocation=full"}, + } + odirectChecker = NewDirectIOChecker(RealOS{}) +) + +func init() { + if err := metrics.SetupMetrics(); err != nil { + klog.Errorf("Unable to create prometheus progress counter: %v", err) + } + ownerUID, _ = util.ParseEnvVar(common.OwnerUID, false) +} + +// NewQEMUOperations returns the default implementation of QEMUOperations +func NewQEMUOperations() QEMUOperations { + return &qemuOperations{} +} + +func convertToRaw(src, dest string, preallocate bool, cacheMode string) error { + cacheMode, err := getCacheMode(dest, cacheMode) + if err != nil { + return err + } + args := []string{"convert", "-t", cacheMode, "-p", "-O", "raw", src, dest} + + if preallocate { + err = addPreallocation(args, convertPreallocationMethods, func(args []string) ([]byte, error) { + return qemuExecFunction(nil, reportProgress, "qemu-img", args...) + }) + } else { + klog.V(1).Infof("Running qemu-img with args: %v", args) + _, err = qemuExecFunction(nil, reportProgress, "qemu-img", args...) + } + if err != nil { + os.Remove(dest) + errorMsg := "could not convert image to raw" + if nbdkitLog, err := os.ReadFile(common.NbdkitLogPath); err == nil { + errorMsg += " " + string(nbdkitLog) + } + return errors.Wrap(err, errorMsg) + } + + return nil +} + +func getCacheMode(path string, cacheMode string) (string, error) { + if cacheMode != common.CacheModeTryNone { + return "writeback", nil + } + + var supportDirectIO bool + isDevice, err := util.IsDevice(path) + if err != nil { + return "", err + } + + if isDevice { + supportDirectIO, err = odirectChecker.CheckBlockDevice(path) + } else { + supportDirectIO, err = odirectChecker.CheckFile(path) + } + if err != nil { + return "", err + } + + if supportDirectIO { + return "none", nil + } + + return "writeback", nil +} + +func (o *qemuOperations) ConvertToRawStream(url *url.URL, dest string, preallocate bool, cacheMode string) error { + if len(url.Scheme) > 0 && url.Scheme != "nbd+unix" { + return fmt.Errorf("not valid schema %s", url.Scheme) + } + return convertToRaw(url.String(), dest, preallocate, cacheMode) +} + +// convertQuantityToQemuSize translates a quantity string into a Qemu compatible string. +func convertQuantityToQemuSize(size resource.Quantity) string { + int64Size, asInt := size.AsInt64() + if !asInt { + size.AsDec().SetScale(0) + return size.AsDec().String() + } + return strconv.FormatInt(int64Size, 10) +} + +// Resize resizes the given image to size +func Resize(image string, size resource.Quantity, preallocate bool) error { + return qemuIterface.Resize(image, size, preallocate) +} + +func (o *qemuOperations) Resize(image string, size resource.Quantity, preallocate bool) error { + var err error + args := []string{"resize", "-f", "raw", image, convertQuantityToQemuSize(size)} + if preallocate { + err = addPreallocation(args, resizePreallocationMethods, func(args []string) ([]byte, error) { + return qemuExecFunction(nil, nil, "qemu-img", args...) + }) + } else { + _, err = qemuExecFunction(nil, nil, "qemu-img", args...) + } + if err != nil { + return errors.Wrapf(err, "Error resizing image %s", image) + } + return nil +} + +func checkOutputQemuImgInfo(output []byte, image string) (*ImgInfo, error) { + var info ImgInfo + err := json.Unmarshal(output, &info) + if err != nil { + klog.Errorf("Invalid JSON:\n%s\n", string(output)) + return nil, errors.Wrapf(err, "Invalid json for image %s", image) + } + return &info, nil +} + +// Info returns information about the image from the url +func Info(url *url.URL) (*ImgInfo, error) { + return qemuIterface.Info(url) +} + +func (o *qemuOperations) Info(url *url.URL) (*ImgInfo, error) { + if len(url.Scheme) > 0 && url.Scheme != "nbd+unix" && url.Scheme != "file" { + return nil, fmt.Errorf("not valid schema %s", url.Scheme) + } + output, err := qemuExecFunction(qemuInfoLimits, nil, "qemu-img", "info", "--output=json", url.String()) + if err != nil { + errorMsg := fmt.Sprintf("%s, %s", output, err.Error()) + if nbdkitLog, err := os.ReadFile(common.NbdkitLogPath); err == nil { + errorMsg += " " + string(nbdkitLog) + } + return nil, errors.Errorf(errorMsg) + } + return checkOutputQemuImgInfo(output, url.String()) +} + +func isSupportedFormat(value string) bool { + switch value { + case "raw", "qcow2", "vmdk", "vdi", "vpc", "vhdx": + return true + default: + return false + } +} + +func checkIfURLIsValid(info *ImgInfo, availableSize int64, image string) error { + if !isSupportedFormat(info.Format) { + return errors.Errorf("Invalid format %s for image %s", info.Format, image) + } + + if len(info.BackingFile) > 0 { + if _, err := os.Stat(info.BackingFile); err != nil { + return errors.Errorf("Image %s is invalid because it has invalid backing file %s", image, info.BackingFile) + } + } + + if availableSize < info.VirtualSize { + return errors.Errorf("virtual image size %d is larger than the reported available storage %d. A larger PVC is required", info.VirtualSize, availableSize) + } + return nil +} + +func (o *qemuOperations) Validate(url *url.URL, availableSize int64) error { + info, err := o.Info(url) + if err != nil { + return err + } + return checkIfURLIsValid(info, availableSize, url.String()) +} + +// ConvertToRawStream converts an http accessible image to raw format without locally caching the image +func ConvertToRawStream(url *url.URL, dest string, preallocate bool, cacheMode string) error { + return qemuIterface.ConvertToRawStream(url, dest, preallocate, cacheMode) +} + +// Validate does basic validation of a qemu image +func Validate(url *url.URL, availableSize int64) error { + return qemuIterface.Validate(url, availableSize) +} + +// convertProgressBase is the offset that the Convert phase contribution starts +// from in the overall import_progress counter (0..100). The TransferScratch +// phase occupies 0..convertProgressBase; the Convert phase fills +// convertProgressBase..100. The actual fraction reported by qemu-img is +// halved when projected (see reportProgress). +const convertProgressBase = 50.0 + +func reportProgress(line string) { + // (45.34/100%) + matches := re.FindStringSubmatch(line) + if len(matches) == 2 && ownerUID != "" { + klog.V(1).Info(matches[1]) + // Don't need to check for an error, the regex made sure its a number we can parse. + v, _ := strconv.ParseFloat(matches[1], 64) + if v <= 0 { + return + } + // Project qemu's 0..100 into convertProgressBase..100. + scaled := convertProgressBase + v*((100.0-convertProgressBase)/100.0) + progress, err := metrics.Progress(ownerUID).Get() + if err == nil && scaled > progress { + metrics.Progress(ownerUID).Add(scaled - progress) + } + } +} + +// CreateBlankImage creates empty raw image +func CreateBlankImage(dest string, size resource.Quantity, preallocate bool) error { + klog.V(1).Infof("creating raw image with size %s, preallocation %v", size.String(), preallocate) + return qemuIterface.CreateBlankImage(dest, size, preallocate) +} + +// CreateBlankImage creates a raw image with a given size +func (o *qemuOperations) CreateBlankImage(dest string, size resource.Quantity, preallocate bool) error { + format, err := util.GetFormat(dest) + if err != nil { + return err + } + + klog.V(3).Infof("image size is %s", size.String()) + args := []string{"create", "-f", format, dest, convertQuantityToQemuSize(size)} + if preallocate { + klog.V(1).Infof("Added preallocation") + args = append(args, []string{"-o", "preallocation=falloc"}...) + } + _, err = qemuExecFunction(nil, nil, "qemu-img", args...) + if err != nil { + os.Remove(dest) + return errors.Wrap(err, fmt.Sprintf("could not create raw image with size %s in %s", size.String(), dest)) + } + // Change permissions to 0660 + err = os.Chmod(dest, 0660) + if err != nil { + err = errors.Wrap(err, "Unable to change permissions of target file") + return err + } + + return nil +} + +func execPreallocationBlock(dest string, bs, count, offset int64) error { + oflag := "oflag=seek_bytes" + supportDirectIO, err := odirectChecker.CheckBlockDevice(dest) + if err != nil { + return err + } + if supportDirectIO { + oflag += ",direct" + } + args := []string{"if=/dev/zero", "of=" + dest, fmt.Sprintf("bs=%d", bs), fmt.Sprintf("count=%d", count), fmt.Sprintf("seek=%d", offset), oflag} + _, err = qemuExecFunction(nil, nil, "dd", args...) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Could not preallocate blank block volume at %s, running dd for size %d, offset %d", dest, bs*count, offset)) + } + + return nil +} + +// PreallocateBlankBlock writes requested amount of zeros to block device mounted at dest +func PreallocateBlankBlock(dest string, size resource.Quantity) error { + klog.V(3).Infof("block volume size is %s", size.String()) + + qemuSize, err := strconv.ParseInt(convertQuantityToQemuSize(size), 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Could not parse size for preallocating blank block volume at %s with size %s", dest, size.String())) + } + countBlocks, remainder := qemuSize/units.MiB, qemuSize%units.MiB + err = execPreallocationBlock(dest, units.MiB, countBlocks, 0) + if err != nil { + return err + } + if remainder != 0 { + return execPreallocationBlock(dest, remainder, 1, countBlocks*units.MiB) + } + return nil +} + +func addPreallocation(args []string, preallocationMethods [][]string, qemuFn func(args []string) ([]byte, error)) error { + var err error + for _, preallocationMethod := range preallocationMethods { + var output []byte + + klog.V(1).Infof("Adding preallocation method: %v", preallocationMethod) + // For some subcommands (e.g. resize), preallocation optinos must come before other options + argsToTry := append([]string{args[0]}, preallocationMethod...) + argsToTry = append(argsToTry, args[1:]...) + klog.V(1).Infof("Attempting preallocation method, qemu-img args: %v", argsToTry) + + output, err = qemuFn(argsToTry) + if err != nil && strings.Contains(string(output), "Unsupported preallocation mode") { + klog.V(1).Infof("Unsupported preallocation mode. Retrying") + } else { + break + } + } + + return err +} + +// Rebase changes a QCOW's backing file to point to a previously-downloaded base image. +// Depends on original image having been downloaded as raw. +func (o *qemuOperations) Rebase(backingFile string, delta string) error { + klog.V(1).Infof("Rebasing %s onto %s", delta, backingFile) + args := []string{"rebase", "-p", "-u", "-F", "raw", "-b", backingFile, delta} + _, err := qemuExecFunction(nil, reportProgress, "qemu-img", args...) + return err +} + +// Commit takes the changes written to a QCOW and applies them to its raw backing file. +func (o *qemuOperations) Commit(image string) error { + klog.V(1).Infof("Committing %s to backing file...", image) + args := []string{"commit", "-p", image} + _, err := qemuExecFunction(nil, reportProgress, "qemu-img", args...) + return err +} diff --git a/images/cdi-artifact/pkg/image/qemu_format_stream.go b/images/cdi-artifact/pkg/image/qemu_format_stream.go new file mode 100644 index 0000000000..e775386bac --- /dev/null +++ b/images/cdi-artifact/pkg/image/qemu_format_stream.go @@ -0,0 +1,49 @@ +package image + +import ( + "fmt" + "net/url" + "os" + + "github.com/pkg/errors" + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" +) + +func convertTo(format, src, dest string, preallocate bool) error { + switch format { + case "qcow2", "raw": + // Do nothing. + default: + return errors.Errorf("unknown format: %s", format) + } + args := []string{"convert", "-t", "writeback", "-p", "-O", format, src, dest} + var err error + + if preallocate { + err = addPreallocation(args, convertPreallocationMethods, func(args []string) ([]byte, error) { + return qemuExecFunction(nil, reportProgress, "qemu-img", args...) + }) + } else { + klog.V(1).Infof("Running qemu-img with args: %v", args) + _, err = qemuExecFunction(nil, reportProgress, "qemu-img", args...) + } + if err != nil { + os.Remove(dest) + errorMsg := fmt.Sprintf("could not convert image to %s", format) + if nbdkitLog, err := os.ReadFile(common.NbdkitLogPath); err == nil { + errorMsg += " " + string(nbdkitLog) + } + return errors.Wrap(err, errorMsg) + } + + return nil +} + +func (o *qemuOperations) ConvertToFormatStream(url *url.URL, format, dest string, preallocate bool) error { + if len(url.Scheme) > 0 && url.Scheme != "nbd+unix" { + return fmt.Errorf("not valid schema %s", url.Scheme) + } + return convertTo(format, url.String(), dest, preallocate) +} diff --git a/images/cdi-artifact/pkg/image/validate.go b/images/cdi-artifact/pkg/image/validate.go new file mode 100644 index 0000000000..845c4834d2 --- /dev/null +++ b/images/cdi-artifact/pkg/image/validate.go @@ -0,0 +1,30 @@ +package image + +const ( + // ExtImg is a constant for the .img extenstion + ExtImg = ".img" + // ExtIso is a constant for the .iso extenstion + ExtIso = ".iso" + // ExtGz is a constant for the .gz extenstion + ExtGz = ".gz" + // ExtQcow2 is a constant for the .qcow2 extenstion + ExtQcow2 = ".qcow2" + // ExtVmdk is a constant for the .vmdk VMware extenstion + ExtVmdk = ".vmdk" + // ExtVdi is a constant for the .vdi VirtualBox extenstion + ExtVdi = ".vdi" + // ExtVhd is a constant for the .vhd Microsoft Virtual Server Virtual Hard Disk extenstion + ExtVhd = ".vhd" + // ExtVhdx is a constant for the .vhd Hyper-V Virtual Hard Disk V.2 extenstion + ExtVhdx = ".vhdx" + // ExtTar is a constant for the .tar extenstion + ExtTar = ".tar" + // ExtXz is a constant for the .xz extenstion + ExtXz = ".xz" + // ExtZst is a constant for the .zst extenstion + ExtZst = ".zst" + // ExtTarXz is a constant for the .tar.xz extenstion + ExtTarXz = ExtTar + ExtXz + // ExtTarGz is a constant for the .tar.gz extenstion + ExtTarGz = ExtTar + ExtGz +) diff --git a/images/cdi-artifact/pkg/importer/data-processor.go b/images/cdi-artifact/pkg/importer/data-processor.go new file mode 100644 index 0000000000..d74b9253c4 --- /dev/null +++ b/images/cdi-artifact/pkg/importer/data-processor.go @@ -0,0 +1,406 @@ +/* +Copyright 2018 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "net/url" + "os" + + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" + "kubevirt.io/containerized-data-importer/pkg/image" + "kubevirt.io/containerized-data-importer/pkg/util" +) + +var qemuOperations = image.NewQEMUOperations() + +// ProcessingPhase is the current phase being processed. +type ProcessingPhase string + +const ( + // ProcessingPhaseInfo is the first phase, during this phase the source obtains information needed to determine which phase to go to next. + ProcessingPhaseInfo ProcessingPhase = "Info" + // ProcessingPhaseTransferScratch is the phase in which the data source writes data to the scratch space. + ProcessingPhaseTransferScratch ProcessingPhase = "TransferScratch" + // ProcessingPhaseTransferDataDir is the phase in which the data source writes data directly to the target path without conversion. + ProcessingPhaseTransferDataDir ProcessingPhase = "TransferDataDir" + // ProcessingPhaseTransferDataFile is the phase in which the data source writes data directly to the target file without conversion. + ProcessingPhaseTransferDataFile ProcessingPhase = "TransferDataFile" + // ProcessingPhaseValidatePause is the phase in which the data processor should validate and then pause. + ProcessingPhaseValidatePause ProcessingPhase = "ValidatePause" + // ProcessingPhaseConvert is the phase in which the data is taken from the url provided by the source, and it is converted to the target RAW disk image format. + // The url can be an http end point or file system end point. + ProcessingPhaseConvert ProcessingPhase = "Convert" + // ProcessingPhaseResize the disk image, this is only needed when the target contains a file system (block device do not need a resize) + ProcessingPhaseResize ProcessingPhase = "Resize" + // ProcessingPhaseComplete is the phase where the entire process completed successfully and we can exit gracefully. + ProcessingPhaseComplete ProcessingPhase = "Complete" + // ProcessingPhasePause is the phase where we pause processing and end the loop, and expect something to call the process loop again. + ProcessingPhasePause ProcessingPhase = "Pause" + // ProcessingPhaseError is the phase in which we encountered an error and need to exit ungracefully. + ProcessingPhaseError ProcessingPhase = common.GenericError + // ProcessingPhaseMergeDelta is the phase in a multi-stage import where a delta image downloaded to scratch is applied to the base image + ProcessingPhaseMergeDelta ProcessingPhase = "MergeDelta" +) + +// may be overridden in tests +var getAvailableSpaceBlockFunc = util.GetAvailableSpaceBlock +var getAvailableSpaceFunc = util.GetAvailableSpace + +// DataSourceInterface is the interface all data sources should implement. +type DataSourceInterface interface { + // Info is called to get initial information about the data. + Info() (ProcessingPhase, error) + // Transfer is called to transfer the data from the source to the path passed in. + Transfer(path string) (ProcessingPhase, error) + // TransferFile is called to transfer the data from the source to the file passed in. + TransferFile(fileName string) (ProcessingPhase, error) + // Geturl returns the url that the data processor can use when converting the data. + GetURL() *url.URL + // GetTerminationMessage returns data to be serialized and used as the termination message of the importer. + GetTerminationMessage() *common.TerminationMessage + // Close closes any readers or other open resources. + Close() error +} + +// ResumableDataSource is the interface all resumeable data sources should implement +type ResumableDataSource interface { + DataSourceInterface + GetResumePhase() ProcessingPhase +} + +// DataProcessor holds the fields needed to process data from a data provider. +type DataProcessor struct { + // currentPhase is the phase the processing is in currently. + currentPhase ProcessingPhase + // provider provides the data for processing. + source DataSourceInterface + // destination file. will be DataDir/disk.img if file system, or a block device (if a block device, then DataDir will not exist). + dataFile string + // dataDir path to target directory if it contains a file system. + dataDir string + // scratchDataDir path to the scratch space. + scratchDataDir string + // requestImageSize is the size we want the resulting image to be. + requestImageSize string + // available space is the available space before downloading the image + availableSpace int64 + // storage overhead is the amount of overhead of the storage used + filesystemOverhead float64 + // preallocation is the flag controlling preallocation setting of qemu-img + preallocation bool + // preallocationApplied is used to pass information whether preallocation has been performed, or not + preallocationApplied bool + // phaseExecutors is a mapping from the given processing phase to its execution function. The function returns the next processing phase or error. + phaseExecutors map[ProcessingPhase]func() (ProcessingPhase, error) + // cacheMode is the mode in which we choose the qemu-img cache mode: + // TRY_NONE = bypass page cache if the target supports it, otherwise, fall back to using page cache + cacheMode string +} + +// NewDataProcessor create a new instance of a data processor using the passed in data provider. +func NewDataProcessor(dataSource DataSourceInterface, dataFile, dataDir, scratchDataDir, requestImageSize string, filesystemOverhead float64, preallocation bool, cacheMode string) *DataProcessor { + dp := &DataProcessor{ + currentPhase: ProcessingPhaseInfo, + source: dataSource, + dataFile: dataFile, + dataDir: dataDir, + scratchDataDir: scratchDataDir, + requestImageSize: requestImageSize, + filesystemOverhead: filesystemOverhead, + preallocation: preallocation, + cacheMode: cacheMode, + } + // Calculate available space before doing anything. + dp.availableSpace = dp.calculateTargetSize() + dp.initDefaultPhases() + return dp +} + +// RegisterPhaseExecutor registers an execution function for the given phase. +// If there is already an function registered, override it with the new function. +func (dp *DataProcessor) RegisterPhaseExecutor(pp ProcessingPhase, executor func() (ProcessingPhase, error)) { + if _, ok := dp.phaseExecutors[pp]; ok { + klog.Warningf("Executor already exists at phase %s. Override it.", pp) + } + dp.phaseExecutors[pp] = executor +} + +// ProcessData is the main synchronous processing loop +func (dp *DataProcessor) ProcessData() error { + return dp.ProcessDataWithPause() +} + +// ProcessDataResume Resume a paused processor, assumes the provided data source is ResumableDataSource +func (dp *DataProcessor) ProcessDataResume() error { + rds, ok := dp.source.(ResumableDataSource) + if !ok { + return errors.New("Datasource not resumable") + } + klog.Infof("Resuming processing at phase %s", rds.GetResumePhase()) + dp.currentPhase = rds.GetResumePhase() + return dp.ProcessDataWithPause() +} + +func (dp *DataProcessor) initDefaultPhases() { + dp.phaseExecutors = make(map[ProcessingPhase]func() (ProcessingPhase, error)) + dp.RegisterPhaseExecutor(ProcessingPhaseInfo, func() (ProcessingPhase, error) { + pp, err := dp.source.Info() + if err != nil { + err = errors.Wrap(err, "Unable to obtain information about data source") + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseTransferScratch, func() (ProcessingPhase, error) { + pp, err := dp.source.Transfer(dp.scratchDataDir) + if errors.Is(err, ErrInvalidPath) { + // Passed in invalid scratch space path, return scratch space needed error. + err = ErrRequiresScratchSpace + } else if err != nil { + err = errors.Wrap(err, "Unable to transfer source data to scratch space") + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseTransferDataDir, func() (ProcessingPhase, error) { + pp, err := dp.source.Transfer(dp.dataDir) + if err != nil { + err = errors.Wrap(err, "Unable to transfer source data to target directory") + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseTransferDataFile, func() (ProcessingPhase, error) { + pp, err := dp.source.TransferFile(dp.dataFile) + if err != nil { + err = errors.Wrap(err, "Unable to transfer source data to target file") + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseValidatePause, func() (ProcessingPhase, error) { + pp := ProcessingPhasePause + err := dp.validate(dp.source.GetURL()) + if err != nil { + pp = ProcessingPhaseError + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseConvert, func() (ProcessingPhase, error) { + pp, err := dp.convert(dp.source.GetURL()) + if err != nil { + err = errors.Wrap(err, "Unable to convert source data to target format") + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseResize, func() (ProcessingPhase, error) { + pp, err := dp.resize() + if err != nil { + err = errors.Wrap(err, "Unable to resize disk image to requested size") + } + return pp, err + }) + dp.RegisterPhaseExecutor(ProcessingPhaseMergeDelta, func() (ProcessingPhase, error) { + pp, err := dp.merge() + if err != nil { + err = errors.Wrap(err, "Unable to apply delta to base image") + } + return pp, err + }) +} + +// ProcessDataWithPause is the main processing loop. +func (dp *DataProcessor) ProcessDataWithPause() error { + visited := make(map[ProcessingPhase]bool, len(dp.phaseExecutors)) + for dp.currentPhase != ProcessingPhaseComplete && dp.currentPhase != ProcessingPhasePause { + if visited[dp.currentPhase] { + err := errors.Errorf("loop detected on phase %s", dp.currentPhase) + klog.Errorf("%+v", err) + return err + } + executor, ok := dp.phaseExecutors[dp.currentPhase] + if !ok { + return errors.Errorf("Unknown processing phase %s", dp.currentPhase) + } + nextPhase, err := executor() + visited[dp.currentPhase] = true + if err != nil { + klog.Errorf("%+v", err) + return err + } + dp.currentPhase = nextPhase + klog.V(1).Infof("New phase: %s\n", dp.currentPhase) + } + return nil +} + +func (dp *DataProcessor) validate(url *url.URL) error { + klog.V(1).Infoln("Validating image") + err := qemuOperations.Validate(url, dp.availableSpace) + if err != nil { + return ValidationSizeError{err: err} + } + return nil +} + +// convert is called when convert the image from the url to a RAW disk image. Source formats include RAW/QCOW2 (Raw to raw conversion is a copy) +func (dp *DataProcessor) convert(url *url.URL) (ProcessingPhase, error) { + err := dp.validate(url) + if err != nil { + return ProcessingPhaseError, err + } + err = CleanAll(dp.dataFile) + if err != nil { + return ProcessingPhaseError, err + } + + format, err := util.GetFormat(dp.dataFile) + if err != nil { + return ProcessingPhaseError, errors.Wrap(err, "Unable to get format") + } + klog.V(3).Infof("Converting to %s", format) + err = qemuOperations.ConvertToFormatStream(url, format, dp.dataFile, dp.preallocation) + if err != nil { + return ProcessingPhaseError, errors.Wrapf(err, "Conversion to %s failed", format) + } + dp.preallocationApplied = dp.preallocation + + return ProcessingPhaseResize, nil +} + +func (dp *DataProcessor) resize() (ProcessingPhase, error) { + size, _ := getAvailableSpaceBlockFunc(dp.dataFile) + klog.V(3).Infof("Available space in dataFile: %d", size) + isBlockDev := size >= int64(0) + if !isBlockDev { + if dp.requestImageSize != "" { + klog.V(3).Infoln("Resizing image") + err := ResizeImage(dp.dataFile, dp.requestImageSize, dp.getUsableSpace(), dp.preallocation) + if err != nil { + return ProcessingPhaseError, errors.Wrap(err, "Resize of image failed") + } + } + // Validate that a sparse file will fit even as it fills out. + dataFileURL, err := url.Parse(dp.dataFile) + if err != nil { + return ProcessingPhaseError, err + } + err = dp.validate(dataFileURL) + if err != nil { + return ProcessingPhaseError, err + } + } + dp.preallocationApplied = dp.preallocation + if dp.dataFile != "" && !isBlockDev { + // Change permissions to 0660 + err := os.Chmod(dp.dataFile, 0660) + if err != nil { + return ProcessingPhaseError, errors.Wrap(err, "Unable to change permissions of target file") + } + } + + return ProcessingPhaseComplete, nil +} + +// ResizeImage resizes the images to match the requested size. Sometimes provisioners misbehave and the available space +// is not the same as the requested space. For those situations we compare the available space to the requested space and +// use the smallest of the two values. +func ResizeImage(dataFile, imageSize string, totalTargetSpace int64, preallocation bool) error { + dataFileURL, _ := url.Parse(dataFile) + info, err := qemuOperations.Info(dataFileURL) + if err != nil { + return err + } + if imageSize != "" { + currentImageSizeQuantity := resource.NewScaledQuantity(info.VirtualSize, 0) + newImageSizeQuantity := resource.MustParse(imageSize) + minSizeQuantity := util.MinQuantity(resource.NewScaledQuantity(totalTargetSpace, 0), &newImageSizeQuantity) + if minSizeQuantity.Cmp(newImageSizeQuantity) != 0 { + // Available destination space is smaller than the size we want to resize to + klog.Warningf("Available space less than requested size, resizing image to available space %s.\n", minSizeQuantity.String()) + } + if currentImageSizeQuantity.Cmp(minSizeQuantity) == 0 { + klog.V(1).Infof("No need to resize image. Requested size: %s, Image size: %d.\n", imageSize, info.VirtualSize) + return nil + } + // Check if calculated size is < imageSize, and return error if so. + if currentImageSizeQuantity.Cmp(minSizeQuantity) == 1 { + klog.V(1).Infof("Calculated new size is < than current size, not resizing: requested size %s, virtual size: %d.\n", minSizeQuantity.String(), info.VirtualSize) + return nil + } + klog.V(1).Infof("Expanding image size to: %s\n", minSizeQuantity.String()) + return qemuOperations.Resize(dataFile, minSizeQuantity, preallocation) + } + return errors.New("Image resize called with blank resize") +} + +func (dp *DataProcessor) calculateTargetSize() int64 { + klog.V(1).Infof("Calculating available size\n") + var targetQuantity *resource.Quantity + size, err := getAvailableSpaceBlockFunc(dp.dataFile) + if err != nil { + klog.Error(err) + } + if size >= int64(0) { + // Block volume. + klog.V(1).Infof("Checking out block volume size.\n") + targetQuantity = resource.NewScaledQuantity(size, 0) + } else { + // File system volume. + klog.V(1).Infof("Checking out file system volume size.\n") + size, err := getAvailableSpaceFunc(dp.dataDir) + if err != nil { + klog.Error(err) + } + targetQuantity = resource.NewScaledQuantity(size, 0) + } + if dp.requestImageSize != "" { + klog.V(1).Infof("Request image size not empty.\n") + newImageSizeQuantity := resource.MustParse(dp.requestImageSize) + minQuantity := util.MinQuantity(targetQuantity, &newImageSizeQuantity) + targetQuantity = &minQuantity + } + klog.V(1).Infof("Target size %s.\n", targetQuantity.String()) + targetSize := targetQuantity.Value() + return targetSize +} + +// PreallocationApplied returns true if data processing path included preallocation step +func (dp *DataProcessor) PreallocationApplied() bool { + return dp.preallocationApplied +} + +func (dp *DataProcessor) getUsableSpace() int64 { + return util.GetUsableSpace(dp.filesystemOverhead, dp.availableSpace) +} + +// Rebase and commit a delta image to its backing file +func (dp *DataProcessor) merge() (ProcessingPhase, error) { + klog.V(1).Info("Merging QCOW to base image.") + imageURL := dp.source.GetURL() + if imageURL == nil { + return ProcessingPhaseError, errors.New("bad URL in data source") + } + if err := qemuOperations.Rebase(dp.dataFile, imageURL.String()); err != nil { + return ProcessingPhaseError, errors.Wrap(err, "error rebasing image") + } + if err := qemuOperations.Commit(imageURL.String()); err != nil { + return ProcessingPhaseError, errors.Wrap(err, "error committing image") + } + return ProcessingPhaseComplete, nil +} diff --git a/images/cdi-artifact/pkg/importer/errors.go b/images/cdi-artifact/pkg/importer/errors.go new file mode 100644 index 0000000000..506bd6b3ac --- /dev/null +++ b/images/cdi-artifact/pkg/importer/errors.go @@ -0,0 +1,42 @@ +package importer + +import ( + "fmt" + + "kubevirt.io/containerized-data-importer/pkg/common" +) + +// ValidationSizeError is an error indication size validation failure. +type ValidationSizeError struct { + err error +} + +func (e ValidationSizeError) Error() string { return e.err.Error() } + +// ErrRequiresScratchSpace indicates that we require scratch space. +var ErrRequiresScratchSpace = fmt.Errorf(common.ScratchSpaceRequired) + +// ErrInvalidPath indicates that the path is invalid. +var ErrInvalidPath = fmt.Errorf("invalid transfer path") + +// ImagePullFailedError indicates that the importer failed to pull an image; This error type wraps the actual error. +type ImagePullFailedError struct { + err error +} + +// NewImagePullFailedError creates new ImagePullFailedError error object, with embedded error. +// +// Use the err parameter fot the actual wrapped error +func NewImagePullFailedError(err error) *ImagePullFailedError { + return &ImagePullFailedError{ + err: err, + } +} + +func (err *ImagePullFailedError) Error() string { + return fmt.Sprintf("%s: %s", common.ImagePullFailureText, err.err.Error()) +} + +func (err *ImagePullFailedError) Unwrap() error { + return err.err +} diff --git a/images/cdi-artifact/pkg/importer/format-readers.go b/images/cdi-artifact/pkg/importer/format-readers.go new file mode 100644 index 0000000000..cb8052a19f --- /dev/null +++ b/images/cdi-artifact/pkg/importer/format-readers.go @@ -0,0 +1,291 @@ +/* +Copyright 2018 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "bytes" + "compress/gzip" + "encoding/hex" + "io" + "strconv" + + "github.com/klauspost/compress/zstd" + "github.com/pkg/errors" + "github.com/ulikunitz/xz" + + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" + "kubevirt.io/containerized-data-importer/pkg/image" + metrics "kubevirt.io/containerized-data-importer/pkg/monitoring/metrics/cdi-importer" + "kubevirt.io/containerized-data-importer/pkg/util" + prometheusutil "kubevirt.io/containerized-data-importer/pkg/util/prometheus" +) + +var ( + ownerUID string +) + +func init() { + if err := metrics.SetupMetrics(); err != nil { + klog.Errorf("Unable to create prometheus progress counter: %v", err) + } + ownerUID, _ = util.ParseEnvVar(common.OwnerUID, false) +} + +type reader struct { + rdrType int + rdr io.ReadCloser +} + +// FormatReaders contains the stack of readers needed to get information from the input stream (io.ReadCloser) +type FormatReaders struct { + readers []reader + buf []byte // holds file headers + Convert bool + Archived bool + ArchiveXz bool + ArchiveGz bool + ArchiveZstd bool + progressReader *prometheusutil.ProgressReader +} + +const ( + rdrGz = iota + rdrMulti + rdrXz + rdrStream +) + +// map scheme and format to rdrType +var rdrTypM = map[string]int{ + "gz": rdrGz, + "xz": rdrXz, + "stream": rdrStream, +} + +// NewFormatReaders creates a new instance of FormatReaders using the input stream and content type passed in. +func NewFormatReaders(stream io.ReadCloser, total uint64) (*FormatReaders, error) { + var err error + readers := &FormatReaders{ + buf: make([]byte, image.MaxExpectedHdrSize), + } + if total > uint64(0) { + readers.progressReader = prometheusutil.NewProgressReader(stream, metrics.Progress(ownerUID), total) + err = readers.constructReaders(readers.progressReader) + } else { + err = readers.constructReaders(stream) + } + return readers, err +} + +func (fr *FormatReaders) constructReaders(r io.ReadCloser) error { + fr.appendReader(rdrTypM["stream"], r) + knownHdrs := image.CopyKnownHdrs() // need local copy since keys are removed + klog.V(3).Infof("constructReaders: checking compression and archive formats\n") + for { + hdr, err := fr.matchHeader(&knownHdrs) + if err != nil { + return errors.WithMessage(err, "could not process image header") + } + if hdr == nil { + break // done processing headers, we have the orig source file + } + klog.V(2).Infof("found header of type %q\n", hdr.Format) + // create format-specific reader and append it to dataStream readers stack + fr.fileFormatSelector(hdr) + // exit loop if hdr is qcow2 + if hdr.Format == "qcow2" { + break + } + } + + return nil +} + +// Append to the receiver's reader stack the passed in reader. If the reader type is multi-reader +// then wrap a multi-reader around the passed in reader. If the reader is not a Closer then wrap a +// nop closer. +func (fr *FormatReaders) appendReader(rType int, x interface{}) { + if x == nil { + return + } + r, ok := x.(io.Reader) + if !ok { + klog.Errorf("internal error: unexpected reader type passed to appendReader()") + return + } + if rType == rdrMulti { + r = io.MultiReader(r, fr.TopReader()) + } + if _, ok := r.(io.Closer); !ok { + r = io.NopCloser(r) + } + fr.readers = append(fr.readers, reader{rdrType: rType, rdr: r.(io.ReadCloser)}) +} + +// TopReader return the top-level io.ReadCloser from the receiver Reader "stack". +func (fr *FormatReaders) TopReader() io.ReadCloser { + return fr.readers[len(fr.readers)-1].rdr +} + +// Based on the passed in header, append the format-specific reader to the readers stack, +// and update the receiver Size field. Note: a bool is set in the receiver for qcow2 files. +func (fr *FormatReaders) fileFormatSelector(hdr *image.Header) { + var r io.Reader + var err error + fFmt := hdr.Format + switch fFmt { + case "gz": + r, err = fr.gzReader() + if err == nil { + fr.Archived = true + fr.ArchiveGz = true + } + case "zst": + r, err = fr.zstReader() + if err == nil { + fr.Archived = true + fr.ArchiveZstd = true + } + case "xz": + r, err = fr.xzReader() + if err == nil { + fr.Archived = true + fr.ArchiveXz = true + } + case "qcow2": + r, err = fr.qcow2NopReader(hdr) + fr.Convert = true + case "vmdk": + r = nil + fr.Convert = true + case "vdi": + r = nil + fr.Convert = true + case "vhd": + r = nil + fr.Convert = true + case "vhdx": + r = nil + fr.Convert = true + } + if err == nil && r != nil { + fr.appendReader(rdrTypM[fFmt], r) + } +} + +// Return the gz reader and the size of the endpoint "through the eye" of the previous reader. +// Assumes a single file was gzipped. +// NOTE: size in gz is stored in the last 4 bytes of the file. This probably requires the file +// +// to be decompressed in order to get its original size. For now 0 is returned. +// +// TODO: support gz size. +func (fr *FormatReaders) gzReader() (io.ReadCloser, error) { + gz, err := gzip.NewReader(fr.TopReader()) + if err != nil { + return nil, errors.Wrap(err, "could not create gzip reader") + } + klog.V(2).Infof("gzip: extracting %q\n", gz.Name) + return gz, nil +} + +// Return the zst reader. +func (fr *FormatReaders) zstReader() (io.ReadCloser, error) { + zst, err := zstd.NewReader(fr.TopReader()) + if err != nil { + return nil, errors.Wrap(err, "could not create zst reader") + } + return zst.IOReadCloser(), nil +} + +// Return the size of the endpoint "through the eye" of the previous reader. Note: there is no +// qcow2 reader so nil is returned so that nothing is appended to the reader stack. +// Note: size is stored at offset 24 in the qcow2 header. +func (fr *FormatReaders) qcow2NopReader(h *image.Header) (io.Reader, error) { + s := hex.EncodeToString(fr.buf[h.SizeOff : h.SizeOff+h.SizeLen]) + _, err := strconv.ParseInt(s, 16, 64) + if err != nil { + return nil, errors.Wrapf(err, "unable to determine original qcow2 file size from %+v", s) + } + return nil, nil +} + +// Return the xz reader and size of the endpoint "through the eye" of the previous reader. +// Assumes a single file was compressed. Note: the xz reader is not a closer so we wrap a +// nop Closer around it. +// NOTE: size is not stored in the xz header. This may require the file to be decompressed in +// +// order to get its original size. For now 0 is returned. +// +// TODO: support gz size. +func (fr *FormatReaders) xzReader() (io.Reader, error) { + xz, err := xz.NewReader(fr.TopReader()) + if err != nil { + return nil, errors.Wrap(err, "could not create xz reader") + } + return xz, nil +} + +// Return the matching header, if one is found, from the passed-in map of known headers. After a +// successful read append a multi-reader to the receiver's reader stack. +// Note: .iso files are not detected here but rather in the Size() function. +// Note: knownHdrs is passed by reference and modified. +func (fr *FormatReaders) matchHeader(knownHdrs *image.Headers) (*image.Header, error) { + _, err := fr.read(fr.buf) // read current header + if err != nil { + return nil, err + } + // append multi-reader so that the header data can be re-read by subsequent readers + fr.appendReader(rdrMulti, bytes.NewReader(fr.buf)) + + // loop through known headers until a match + for format, kh := range *knownHdrs { + if kh.Match(fr.buf) { + // delete this header format key so that it's not processed again + delete(*knownHdrs, format) + return &kh, nil + } + } + return nil, nil // no match +} + +// Read from top-most reader. Note: ReadFull is needed since there may be intermediate, +// smaller multi-readers in the reader stack, and we need to be able to fill buf. +func (fr *FormatReaders) read(buf []byte) (int, error) { + return io.ReadFull(fr.TopReader(), buf) +} + +// Close Readers in reverse order. +func (fr *FormatReaders) Close() (rtnerr error) { + var err error + for i := len(fr.readers) - 1; i >= 0; i-- { + err = fr.readers[i].rdr.Close() + if err != nil { + rtnerr = err // tracking last error + } + } + return rtnerr +} + +// StartProgressUpdate starts the go routine to automatically update the progress on a set interval. +func (fr *FormatReaders) StartProgressUpdate() { + if fr.progressReader != nil { + fr.progressReader.StartTimedUpdate() + } +} diff --git a/images/cdi-artifact/pkg/importer/registry-datasource.go b/images/cdi-artifact/pkg/importer/registry-datasource.go new file mode 100644 index 0000000000..4fd5a0a394 --- /dev/null +++ b/images/cdi-artifact/pkg/importer/registry-datasource.go @@ -0,0 +1,219 @@ +/* +Copyright 2018 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/containers/image/v5/types" + "github.com/pkg/errors" + + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" + "kubevirt.io/containerized-data-importer/pkg/util" +) + +const ( + // containerDiskImageDir - Expected disk image location in container image as described in + // https://github.com/kubevirt/kubevirt/blob/main/docs/container-register-disks.md + containerDiskImageDir = "disk" +) + +// RegistryDataSource is the struct containing the information needed to import from a registry data source. +// Sequence of phases: +// 1. Info -> Transfer +// 2. Transfer -> Convert +type RegistryDataSource struct { + endpoint string + accessKey string + secKey string + certDir string + insecureTLS bool + imageDir string + //The discovered image file in scratch space. + url *url.URL + //The discovered image info from the registry. + info *types.ImageInspectInfo +} + +// NewRegistryDataSource creates a new instance of the Registry Data Source. +func NewRegistryDataSource(endpoint, accessKey, secKey, certDir string, insecureTLS bool) *RegistryDataSource { + allCertDir, err := CreateCertificateDir(certDir) + if err != nil { + klog.Infof("Error creating allCertDir %v", err) + if allCertDir != "/" { + err = os.RemoveAll(allCertDir) + if err != nil { + klog.Errorf("Unable to clean up all cert dir %v", err) + } + } + allCertDir = certDir + } + return &RegistryDataSource{ + endpoint: endpoint, + accessKey: accessKey, + secKey: secKey, + certDir: allCertDir, + insecureTLS: insecureTLS, + } +} + +// Info is called to get initial information about the data. No information available for registry currently. +func (rd *RegistryDataSource) Info() (ProcessingPhase, error) { + return ProcessingPhaseTransferScratch, nil +} + +// Transfer is called to transfer the data from the source registry to a temporary location. +func (rd *RegistryDataSource) Transfer(path string) (ProcessingPhase, error) { + rd.imageDir = filepath.Join(path, containerDiskImageDir) + if err := CleanAll(rd.imageDir); err != nil { + return ProcessingPhaseError, err + } + + size, err := util.GetAvailableSpace(path) + if err != nil { + return ProcessingPhaseError, err + } + if size <= int64(0) { + //Path provided is invalid. + return ProcessingPhaseError, ErrInvalidPath + } + + klog.V(1).Infof("Copying registry image to scratch space.") + rd.info, err = CopyRegistryImage(rd.endpoint, path, containerDiskImageDir, rd.accessKey, rd.secKey, rd.certDir, rd.insecureTLS) + if err != nil { + return ProcessingPhaseError, errors.Wrapf(err, "Failed to read registry image") + } + + imageFile, err := getImageFileName(rd.imageDir) + if err != nil { + return ProcessingPhaseError, errors.Wrapf(err, "Cannot locate image file") + } + + // imageFile and rd.imageDir are both valid, thus the Join will be valid, and the parse will work, no need to check for parse errors + rd.url, _ = url.Parse(filepath.Join(rd.imageDir, imageFile)) + klog.V(3).Infof("Successfully found file. VM disk image filename is %s", rd.url.String()) + return ProcessingPhaseConvert, nil +} + +// TransferFile is called to transfer the data from the source to the passed in file. +func (rd *RegistryDataSource) TransferFile(fileName string) (ProcessingPhase, error) { + return ProcessingPhaseError, errors.New("Transferfile should not be called") +} + +// GetURL returns the url that the data processor can use when converting the data. +func (rd *RegistryDataSource) GetURL() *url.URL { + return rd.url +} + +// GetTerminationMessage returns data to be serialized and used as the termination message of the importer. +func (rd *RegistryDataSource) GetTerminationMessage() *common.TerminationMessage { + if rd.info == nil { + return nil + } + return &common.TerminationMessage{ + Labels: envsToLabels(rd.info.Env), + } +} + +// Close closes any readers or other open resources. +func (rd *RegistryDataSource) Close() error { + // No-op, no open readers + return nil +} + +func getImageFileName(dir string) (string, error) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + klog.Errorf("image directory does not exist") + return "", errors.Errorf("image directory does not exist") + } + + entries, err := os.ReadDir(dir) + if err != nil { + klog.Errorf("Error reading directory") + return "", errors.Wrapf(err, "image file does not exist in image directory") + } + + if len(entries) == 0 { + klog.Errorf("image file does not exist in image directory - directory is empty ") + return "", errors.New("image file does not exist in image directory - directory is empty") + } + + if len(entries) > 1 { + klog.Errorf("image directory contains more than one file") + return "", errors.New("image directory contains more than one file") + } + + fileinfo := entries[0] + if fileinfo.IsDir() { + klog.Errorf("image file does not exist in image directory contains another directory ") + return "", errors.New("image directory contains another directory") + } + + filename := fileinfo.Name() + + if len(strings.TrimSpace(filename)) == 0 { + klog.Errorf("image file does not exist in image directory - file has no name ") + return "", errors.New("image file does has no name") + } + + klog.V(1).Infof("VM disk image filename is %s", filename) + + return filename, nil +} + +// CreateCertificateDir creates a common certificate dir +func CreateCertificateDir(registryCertDir string) (string, error) { + allCerts := "/tmp/all_certs" + if err := os.MkdirAll(allCerts, 0700); err != nil { + return allCerts, err + } + + klog.Info("Copying proxy certs") + if err := collectCerts(common.ImporterProxyCertDir, allCerts, "proxy-"); err != nil { + return allCerts, err + } + klog.Info("Copying registry certs") + if err := collectCerts(registryCertDir, allCerts, ""); err != nil { + return allCerts, err + } + return allCerts, nil +} + +func collectCerts(certDir, targetDir, targetPrefix string) error { + directory, err := os.Open(certDir) + if err != nil { + return err + } + objects, err := directory.Readdir(-1) + if err != nil { + return err + } + for _, obj := range objects { + if !strings.HasSuffix(obj.Name(), ".crt") { + continue + } + if err := util.LinkFile(filepath.Join(certDir, obj.Name()), filepath.Join(targetDir, targetPrefix+obj.Name())); err != nil { + return err + } + } + return nil +} diff --git a/images/cdi-artifact/pkg/importer/transport.go b/images/cdi-artifact/pkg/importer/transport.go new file mode 100644 index 0000000000..e2646734c8 --- /dev/null +++ b/images/cdi-artifact/pkg/importer/transport.go @@ -0,0 +1,315 @@ +/* +Copyright 2020 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "archive/tar" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/image" + "github.com/containers/image/v5/oci/archive" + "github.com/containers/image/v5/pkg/blobinfocache" + "github.com/containers/image/v5/types" + "github.com/pkg/errors" + + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" + metrics "kubevirt.io/containerized-data-importer/pkg/monitoring/metrics/cdi-importer" + "kubevirt.io/containerized-data-importer/pkg/util" + prometheusutil "kubevirt.io/containerized-data-importer/pkg/util/prometheus" +) + +const ( + whFilePrefix = ".wh." + + registrySchemeDocker = "docker" + registrySchemeOCI = "oci-archive" + + // transferScratchMaxProgress is the upper bound of the TransferScratch + // phase contribution to the overall import progress (0..100). The remaining + // half (50..100) is occupied by the Convert phase, see qemu.reportProgress. + transferScratchMaxProgress = 49.0 +) + +// scaledProgressMetric adapts a 0..100 prometheus.ProgressMetric so that the +// underlying counter advances only within the [low, high] sub-range. It is +// used to project the per-layer download progress reported by +// prometheusutil.ProgressReader (which always works in 0..100) into the +// 0..transferScratchMaxProgress slice of the shared import_progress counter. +type scaledProgressMetric struct { + base prometheusutil.ProgressMetric + low, high float64 +} + +func (s scaledProgressMetric) Get() (float64, error) { + cur, err := s.base.Get() + if err != nil { + return 0, err + } + if cur <= s.low { + return 0, nil + } + if cur >= s.high { + return 100, nil + } + return (cur - s.low) * 100.0 / (s.high - s.low), nil +} + +func (s scaledProgressMetric) Add(delta float64) { + if delta <= 0 { + return + } + s.base.Add(delta * (s.high - s.low) / 100.0) +} + +func (s scaledProgressMetric) Delete() { s.base.Delete() } + +// transferProgressMetric returns a 0..transferScratchMaxProgress projection of +// the shared import_progress counter that ProgressReader can drive in 0..100. +func transferProgressMetric() prometheusutil.ProgressMetric { + ownerUID, _ := util.ParseEnvVar(common.OwnerUID, false) + return scaledProgressMetric{ + base: metrics.Progress(ownerUID), + low: 0, + high: transferScratchMaxProgress, + } +} + +func commandTimeoutContext() (context.Context, context.CancelFunc) { + return context.WithCancel(context.Background()) +} + +func buildSourceContext(accessKey, secKey, certDir string, insecureRegistry bool) *types.SystemContext { + ctx := &types.SystemContext{} + if accessKey != "" && secKey != "" { + ctx.DockerAuthConfig = &types.DockerAuthConfig{ + Username: accessKey, + Password: secKey, + } + } + if certDir != "" { + ctx.DockerCertPath = certDir + ctx.DockerDaemonCertPath = certDir + } + + if insecureRegistry { + ctx.DockerDaemonInsecureSkipTLSVerify = true + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true) + } + + return ctx +} + +func readImageSource(ctx context.Context, sys *types.SystemContext, img string) (types.ImageSource, error) { + ref, err := parseImageName(img) + if err != nil { + klog.Errorf("Could not parse image: %v", err) + return nil, errors.Wrap(err, "Could not parse image") + } + + src, err := ref.NewImageSource(ctx, sys) + if err != nil { + klog.Errorf("Could not create image reference: %v", err) + return nil, NewImagePullFailedError(err) + } + + return src, nil +} + +func parseImageName(img string) (types.ImageReference, error) { + parts := strings.SplitN(img, ":", 2) + if len(parts) != 2 { + return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, img) + } + switch parts[0] { + case registrySchemeDocker: + return docker.ParseReference(parts[1]) + case registrySchemeOCI: + return archive.ParseReference(parts[1]) + } + return nil, errors.Errorf(`Invalid image name "%s", unknown transport`, img) +} + +func closeImage(src types.ImageSource) { + if err := src.Close(); err != nil { + klog.Warningf("Could not close image source: %v ", err) + } +} + +func hasPrefix(path string, pathPrefix string) bool { + return strings.HasPrefix(path, pathPrefix) || + strings.HasPrefix(path, "./"+pathPrefix) +} + +func isWhiteout(path string) bool { + return strings.HasPrefix(filepath.Base(path), whFilePrefix) +} + +func isDir(hdr *tar.Header) bool { + return hdr.Typeflag == tar.TypeDir +} + +func processLayer(ctx context.Context, + sys *types.SystemContext, + src types.ImageSource, + layer types.BlobInfo, + destDir string, + pathPrefix string, + cache types.BlobInfoCache, + stopAtFirst bool) (bool, error) { + var reader io.ReadCloser + reader, _, err := src.GetBlob(ctx, layer, cache) + if err != nil { + klog.Errorf("Could not read layer: %v", err) + return false, errors.Wrap(err, "Could not read layer") + } + // Track download progress of the current layer in the lower half of the + // shared import_progress counter (0..transferScratchMaxProgress). The + // Convert phase later fills 50..100. + if layer.Size > 0 { + progressReader := prometheusutil.NewProgressReader(reader, transferProgressMetric(), uint64(layer.Size)) + progressReader.StartTimedUpdate() + reader = progressReader + } + fr, err := NewFormatReaders(reader, 0) + if err != nil { + return false, errors.Wrap(err, "Could not read layer") + } + defer fr.Close() + + tarReader := tar.NewReader(fr.TopReader()) + found := false + for { + hdr, err := tarReader.Next() + if errors.Is(err, io.EOF) { + break // End of archive + } + if err != nil { + klog.Errorf("Error reading layer: %v", err) + return false, errors.Wrap(err, "Error reading layer") + } + + if hasPrefix(hdr.Name, pathPrefix) && !isWhiteout(hdr.Name) && !isDir(hdr) { + klog.Infof("File '%v' found in the layer", hdr.Name) + destFile, err := safeJoinPaths(destDir, hdr.Name) + if err != nil { + klog.Errorf("Error sanitizing archive path: %v", err) + return false, errors.Wrap(err, "Error sanitizing archive path") + } + + if err = os.MkdirAll(filepath.Dir(destFile), os.ModePerm); err != nil { + klog.Errorf("Error creating output file's directory: %v", err) + return false, errors.Wrap(err, "Error creating output file's directory") + } + + if err := streamDataToFile(tarReader, destFile); err != nil { + klog.Errorf("Error copying file: %v", err) + return false, errors.Wrap(err, "Error copying file") + } + + found = true + if stopAtFirst { + return found, nil + } + } + } + + return found, nil +} + +// Sanitize archive file pathing from "G305: Zip Slip vulnerability" +// https://security.snyk.io/research/zip-slip-vulnerability +func safeJoinPaths(dir, path string) (v string, err error) { + v = filepath.Join(dir, path) + wantPrefix := filepath.Clean(dir) + string(os.PathSeparator) + + if strings.HasPrefix(v, wantPrefix) { + return v, nil + } + + return "", fmt.Errorf("%s: %s", "content filepath is tainted", path) +} + +func copyRegistryImage(url, destDir, pathPrefix, accessKey, secKey, certDir string, insecureRegistry, stopAtFirst bool) (*types.ImageInspectInfo, error) { + klog.Infof("Downloading image from '%v', copying file from '%v' to '%v'", url, pathPrefix, destDir) + + ctx, cancel := commandTimeoutContext() + defer cancel() + srcCtx := buildSourceContext(accessKey, secKey, certDir, insecureRegistry) + + src, err := readImageSource(ctx, srcCtx, url) + if err != nil { + return nil, err + } + defer closeImage(src) + + imgCloser, err := image.FromSource(ctx, srcCtx, src) + if err != nil { + klog.Errorf("Error retrieving image: %v", err) + return nil, errors.Wrap(err, "Error retrieving image") + } + defer imgCloser.Close() + + cache := blobinfocache.DefaultCache(srcCtx) + found := false + layers := imgCloser.LayerInfos() + + for _, layer := range layers { + klog.Infof("Processing layer %+v", layer) + + found, err = processLayer(ctx, srcCtx, src, layer, destDir, pathPrefix, cache, stopAtFirst) + if found { + break + } + if err != nil { + // Skipping layer and trying the next one. + // Error already logged in processLayer + continue + } + } + + if !found { + klog.Errorf("Failed to find VM disk image file in the container image") + return nil, errors.New("Failed to find VM disk image file in the container image") + } + + info, err := imgCloser.Inspect(ctx) + if err != nil { + return nil, err + } + + return info, nil +} + +// CopyRegistryImage download image from registry with docker image API. It will extract first file under the pathPrefix +// url: source registry url. +// destDir: the scratch space destination. +// pathPrefix: path to extract files from. +// accessKey: accessKey for the registry described in url. +// secKey: secretKey for the registry described in url. +// certDir: directory public CA keys are stored for registry identity verification +// insecureRegistry: boolean if true will allow insecure registries. +func CopyRegistryImage(url, destDir, pathPrefix, accessKey, secKey, certDir string, insecureRegistry bool) (*types.ImageInspectInfo, error) { + return copyRegistryImage(url, destDir, pathPrefix, accessKey, secKey, certDir, insecureRegistry, true) +} diff --git a/images/cdi-artifact/pkg/importer/util.go b/images/cdi-artifact/pkg/importer/util.go new file mode 100644 index 0000000000..8090b69f33 --- /dev/null +++ b/images/cdi-artifact/pkg/importer/util.go @@ -0,0 +1,107 @@ +package importer + +import ( + "io" + "net/url" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/pkg/errors" + + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/common" + "kubevirt.io/containerized-data-importer/pkg/util" +) + +const ( + kubevirtEnvPrefix = "KUBEVIRT_IO_" + kubevirtLabelPrefix = "kubevirt.io/" +) + +// ParseEndpoint parses the required endpoint and return the url struct. +func ParseEndpoint(endpt string) (*url.URL, error) { + if endpt == "" { + // Because we are passing false, we won't decode anything and there is no way to error. + endpt, _ = util.ParseEnvVar(common.ImporterEndpoint, false) + if endpt == "" { + return nil, errors.Errorf("endpoint %q is missing or blank", common.ImporterEndpoint) + } + } + return url.Parse(endpt) +} + +// CleanAll deletes all files at specified paths (recursively) +func CleanAll(paths ...string) error { + for _, p := range paths { + isDevice, err := util.IsDevice(p) + if err != nil { + return err + } + + if !isDevice { + // Remove handles p not existing + if err := os.RemoveAll(p); err != nil { + return err + } + } + } + return nil +} + +// GetTerminationChannel returns a channel that listens for SIGTERM +func GetTerminationChannel() <-chan os.Signal { + terminationChannel := make(chan os.Signal, 1) + signal.Notify(terminationChannel, os.Interrupt, syscall.SIGTERM) + return terminationChannel +} + +// newTerminationChannel should be overridden for unit tests +var newTerminationChannel = GetTerminationChannel + +func envsToLabels(envs []string) map[string]string { + labels := map[string]string{} + for _, env := range envs { + k, v, found := strings.Cut(env, "=") + if !found || !strings.Contains(k, kubevirtEnvPrefix) { + continue + } + labels[envToLabel(k)] = v + } + + return labels +} + +func envToLabel(env string) string { + label := "" + before, after, _ := strings.Cut(env, kubevirtEnvPrefix) + if elems := strings.Split(strings.TrimSuffix(before, "_"), "_"); len(elems) > 0 && elems[0] != "" { + label += strings.Join(elems, ".") + "." + } + label += kubevirtLabelPrefix + label += strings.Join(strings.Split(after, "_"), "-") + + return strings.ToLower(label) +} + +// streamDataToFile provides a function to stream the specified io.Reader to the specified local file +func streamDataToFile(r io.Reader, fileName string) error { + outFile, err := util.OpenFileOrBlockDevice(fileName) + if err != nil { + return err + } + defer outFile.Close() + klog.V(1).Infof("Writing data...\n") + if _, err = io.Copy(outFile, r); err != nil { + klog.Errorf("Unable to write file from dataReader: %v\n", err) + os.Remove(outFile.Name()) + if strings.Contains(err.Error(), "no space left on device") { + return errors.Wrapf(err, "unable to write to file") + } + return NewImagePullFailedError(err) + } + err = outFile.Sync() + return err +} diff --git a/images/cdi-artifact/pkg/monitoring/metrics/cdi-importer/import_metrics.go b/images/cdi-artifact/pkg/monitoring/metrics/cdi-importer/import_metrics.go new file mode 100644 index 0000000000..ab53a05b92 --- /dev/null +++ b/images/cdi-artifact/pkg/monitoring/metrics/cdi-importer/import_metrics.go @@ -0,0 +1,52 @@ +package cdiimporter + +import ( + "github.com/machadovilaca/operator-observability/pkg/operatormetrics" + ioprometheusclient "github.com/prometheus/client_model/go" +) + +const ( + // ImportProgressMetricName is the name of the import progress metric + ImportProgressMetricName = "kubevirt_cdi_import_progress_total" +) + +var ( + importerMetrics = []operatormetrics.Metric{ + importProgress, + } + + importProgress = operatormetrics.NewCounterVec( + operatormetrics.MetricOpts{ + Name: ImportProgressMetricName, + Help: "The import progress in percentage", + }, + []string{"ownerUID"}, + ) +) + +type ImportProgress struct { + ownerUID string +} + +func Progress(ownerUID string) *ImportProgress { + return &ImportProgress{ownerUID} +} + +// Add adds value to the importProgress metric +func (ip *ImportProgress) Add(value float64) { + importProgress.WithLabelValues(ip.ownerUID).Add(value) +} + +// Get returns the importProgress value +func (ip *ImportProgress) Get() (float64, error) { + dto := &ioprometheusclient.Metric{} + if err := importProgress.WithLabelValues(ip.ownerUID).Write(dto); err != nil { + return 0, err + } + return dto.Counter.GetValue(), nil +} + +// Delete removes the importProgress metric with the passed label +func (ip *ImportProgress) Delete() { + importProgress.DeleteLabelValues(ip.ownerUID) +} diff --git a/images/cdi-artifact/pkg/monitoring/metrics/cdi-importer/metrics.go b/images/cdi-artifact/pkg/monitoring/metrics/cdi-importer/metrics.go new file mode 100644 index 0000000000..63b1b11104 --- /dev/null +++ b/images/cdi-artifact/pkg/monitoring/metrics/cdi-importer/metrics.go @@ -0,0 +1,12 @@ +package cdiimporter + +import ( + "github.com/machadovilaca/operator-observability/pkg/operatormetrics" +) + +// SetupMetrics register prometheus metrics +func SetupMetrics() error { + return operatormetrics.RegisterMetrics( + importerMetrics, + ) +} diff --git a/images/cdi-artifact/pkg/system/prlimit.go b/images/cdi-artifact/pkg/system/prlimit.go new file mode 100644 index 0000000000..4971ef8a8d --- /dev/null +++ b/images/cdi-artifact/pkg/system/prlimit.go @@ -0,0 +1,193 @@ +/* +Copyright 2018 The CDI Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package system + +import ( + "bufio" + "bytes" + "context" + "os/exec" + "syscall" + "time" + "unsafe" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" + + "k8s.io/klog/v2" +) + +// ProcessLimiter defines the methods limiting resources of a Process +type ProcessLimiter interface { + SetAddressSpaceLimit(pid int, value uint64) error + SetCPUTimeLimit(pid int, value uint64) error +} + +// ProcessLimitValues specifies the resource limits available to a process +type ProcessLimitValues struct { + AddressSpaceLimit uint64 + CPUTimeLimit uint64 +} + +type processLimiter struct{} + +var execCommand = exec.Command +var execCommandContext = exec.CommandContext + +var limiter = NewProcessLimiter() + +// NewProcessLimiter returns a new ProcessLimiter +func NewProcessLimiter() ProcessLimiter { + return &processLimiter{} +} + +func (p *processLimiter) SetAddressSpaceLimit(pid int, value uint64) error { + return prlimit(pid, unix.RLIMIT_AS, &syscall.Rlimit{Cur: value, Max: value}) +} + +func (p *processLimiter) SetCPUTimeLimit(pid int, value uint64) error { + return prlimit(pid, unix.RLIMIT_CPU, &syscall.Rlimit{Cur: value, Max: value}) +} + +// SetAddressSpaceLimit sets a limit on total address space of a process +func SetAddressSpaceLimit(pid int, value uint64) error { + return limiter.SetAddressSpaceLimit(pid, value) +} + +// SetCPUTimeLimit sets a limit on the total cpu time a process may have +func SetCPUTimeLimit(pid int, value uint64) error { + return limiter.SetCPUTimeLimit(pid, value) +} + +// scanLinesWithCR is an alternate split function that works with carriage returns as well +// as new lines. +func scanLinesWithCR(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\r'); i >= 0 { + // We have a full carriage return-terminated line. + return i + 1, data[0:i], nil + } + if i := bytes.IndexByte(data, '\n'); i >= 0 { + // We have a full newline-terminated line. + return i + 1, data[0:i], nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} + +func processScanner(scanner *bufio.Scanner, buf *bytes.Buffer, done chan bool, callback func(string)) { + for scanner.Scan() { + line := scanner.Text() + buf.WriteString(line) + buf.WriteString("\n") + if callback != nil { + callback(line) + } + } + done <- true +} + +// ExecWithLimits executes a command with process limits +func ExecWithLimits(limits *ProcessLimitValues, callback func(string), command string, args ...string) ([]byte, error) { + return executeWithLimits(limits, callback, true, command, args...) +} + +// ExecWithLimitsSilently executes a command with process limits and do not print output on error +func ExecWithLimitsSilently(limits *ProcessLimitValues, callback func(string), command string, args ...string) ([]byte, error) { + return executeWithLimits(limits, callback, false, command, args...) +} + +func executeWithLimits(limits *ProcessLimitValues, callback func(string), logErr bool, command string, args ...string) ([]byte, error) { + // Args can potentially contain sensitive information, make sure NOT to write args to the logs. + var buf, errBuf bytes.Buffer + var cmd *exec.Cmd + + stdoutDone := make(chan bool) + stderrDone := make(chan bool) + + if limits != nil && limits.CPUTimeLimit > 0 { + klog.V(3).Infof("Setting CPU limit to %d\n", limits.CPUTimeLimit) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(limits.CPUTimeLimit)*time.Second) + defer cancel() + cmd = execCommandContext(ctx, command, args...) + } else { + cmd = execCommand(command, args...) + } + stdoutIn, err := cmd.StdoutPipe() + if err != nil { + return nil, errors.Wrapf(err, "Couldn't get stdout for %s", command) + } + stderrIn, err := cmd.StderrPipe() + if err != nil { + return nil, errors.Wrapf(err, "Couldn't get stderr for %s", command) + } + + scanner := bufio.NewScanner(stdoutIn) + scanner.Split(scanLinesWithCR) + errScanner := bufio.NewScanner(stderrIn) + errScanner.Split(scanLinesWithCR) + + err = cmd.Start() + if err != nil { + return nil, errors.Wrapf(err, "Couldn't start %s", command) + } + defer func() { + err = cmd.Process.Kill() + klog.Errorf("failed to kill the process; %v", err) + }() + + go processScanner(scanner, &buf, stdoutDone, callback) + go processScanner(errScanner, &errBuf, stderrDone, callback) + + if limits != nil && limits.AddressSpaceLimit > 0 { + klog.V(3).Infof("Setting Address space limit to %d\n", limits.AddressSpaceLimit) + err = SetAddressSpaceLimit(cmd.Process.Pid, limits.AddressSpaceLimit) + if err != nil { + return nil, errors.Wrap(err, "Couldn't set address space limit") + } + } + <-stdoutDone + <-stderrDone + // The wait has to be after the reading channels are finished otherwise there is a race where the wait completes and closes stdout/err before anything + // is read from it. + err = cmd.Wait() + + output := buf.Bytes() + if err != nil { + if logErr { + klog.Errorf("%s failed output is:\n", command) + klog.Errorf("%s\n", string(output)) + klog.Errorf("%s\n", errBuf.String()) + } + return errBuf.Bytes(), errors.Wrapf(err, "%s execution failed", command) + } + return output, nil +} + +func prlimit(pid int, limit int, value *syscall.Rlimit) error { + _, _, e1 := syscall.RawSyscall6(syscall.SYS_PRLIMIT64, uintptr(pid), uintptr(limit), uintptr(unsafe.Pointer(value)), 0, 0, 0) + if e1 != 0 { + return errors.Wrapf(e1, "error setting prlimit on %d with value %d on pid %d", limit, value, pid) + } + return nil +} diff --git a/images/cdi-artifact/pkg/util/file.go b/images/cdi-artifact/pkg/util/file.go new file mode 100644 index 0000000000..6e92d15139 --- /dev/null +++ b/images/cdi-artifact/pkg/util/file.go @@ -0,0 +1,318 @@ +package util + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" + + "k8s.io/klog/v2" +) + +// OpenFileOrBlockDevice opens the destination data file, whether it is a block device or regular file +func OpenFileOrBlockDevice(fileName string) (*os.File, error) { + var outFile *os.File + blockSize, err := GetAvailableSpaceBlock(fileName) + if err != nil { + return nil, errors.Wrapf(err, "error determining if block device exists") + } + if blockSize >= 0 { + // Block device found and size determined. + outFile, err = os.OpenFile(fileName, os.O_EXCL|os.O_WRONLY, os.ModePerm) + } else { + // Attempt to create the file with name filePath. If it exists, fail. + outFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.ModePerm) + } + if err != nil { + return nil, errors.Wrapf(err, "could not open file %q", fileName) + } + return outFile, nil +} + +// CopyFile copies a file from one location to another. +func CopyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} + +// LinkFile symlinks the source to the target +func LinkFile(source, target string) error { + out, err := exec.Command("/usr/bin/ln", "-s", source, target).CombinedOutput() + if err != nil { + fmt.Printf("out [%s]\n", string(out)) + return err + } + return nil +} + +// CopyDir copies a dir from one location to another. +func CopyDir(source string, dest string) error { + // get properties of source dir + sourceinfo, err := os.Stat(source) + if err != nil { + return err + } + + // create dest dir + err = os.MkdirAll(dest, sourceinfo.Mode()) + if err != nil { + return err + } + + directory, _ := os.Open(source) + objects, err := directory.Readdir(-1) + + for _, obj := range objects { + src := filepath.Join(source, obj.Name()) + dst := filepath.Join(dest, obj.Name()) + + if obj.IsDir() { + // create sub-directories - recursively + err = CopyDir(src, dst) + if err != nil { + fmt.Println(err) + } + } else { + // perform copy + err = CopyFile(src, dst) + if err != nil { + fmt.Println(err) + } + } + } + return err +} + +// GetAvailableSpace gets the amount of available space at the path specified. +func GetAvailableSpace(path string) (int64, error) { + var stat syscall.Statfs_t + err := syscall.Statfs(path, &stat) + if err != nil { + return int64(-1), err + } + return int64(stat.Bavail) * stat.Bsize, nil +} + +// GetAvailableSpaceBlock gets the amount of available space at the block device path specified. +func GetAvailableSpaceBlock(deviceName string) (int64, error) { + // Check if the file exists and is a device file. + if ok, err := IsDevice(deviceName); !ok || err != nil { + return int64(-1), err + } + + // Device exists, attempt to get size. + cmd := exec.Command(blockdevFileName, "--getsize64", deviceName) + var out bytes.Buffer + var errBuf bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errBuf + err := cmd.Run() + if err != nil { + return int64(-1), errors.Errorf("%v, %s", err, errBuf.String()) + } + i, err := strconv.ParseInt(strings.TrimSpace(out.String()), 10, 64) + if err != nil { + return int64(-1), err + } + return i, nil +} + +// IsDevice returns true if it's a device file +func IsDevice(deviceName string) (bool, error) { + info, err := os.Stat(deviceName) + if err == nil { + return (info.Mode() & os.ModeDevice) != 0, nil + } + + if os.IsNotExist(err) { + return false, nil + } + + return false, err +} + +// Three functions for zeroing a range in the destination file: + +// PunchHole attempts to zero a range in a file with fallocate, for block devices and pre-allocated files. +func PunchHole(outFile *os.File, start, length int64) error { + klog.V(4).Infof("Punching %d-byte hole at offset %d", length, start) + flags := uint32(unix.FALLOC_FL_PUNCH_HOLE | unix.FALLOC_FL_KEEP_SIZE) + err := syscall.Fallocate(int(outFile.Fd()), flags, start, length) + if err == nil { + _, err = outFile.Seek(length, io.SeekCurrent) // Just to move current file position + } + return err +} + +// AppendZeroWithTruncate resizes the file to append zeroes, meant only for newly-created (empty and zero-length) regular files. +func AppendZeroWithTruncate(outFile *os.File, start, length int64) error { + klog.V(4).Infof("Truncating %d-bytes from offset %d", length, start) + end, err := outFile.Seek(0, io.SeekEnd) + if err != nil { + return err + } + if start != end { + return errors.Errorf("starting offset %d does not match previous ending offset %d, cannot safely append zeroes to this file using truncate", start, end) + } + err = outFile.Truncate(start + length) + if err != nil { + return err + } + _, err = outFile.Seek(0, io.SeekEnd) + return err +} + +var zeroBuffer []byte + +// AppendZeroWithWrite just does normal file writes to the destination, a slow but reliable fallback option. +func AppendZeroWithWrite(outFile *os.File, start, length int64) error { + klog.Infof("Writing %d zero bytes at offset %d", length, start) + offset, err := outFile.Seek(0, io.SeekCurrent) + if err != nil { + return err + } + if start != offset { + return errors.Errorf("starting offset %d does not match previous ending offset %d, cannot safely append zeroes to this file using write", start, offset) + } + if zeroBuffer == nil { // No need to re-allocate this on every write + zeroBuffer = bytes.Repeat([]byte{0}, 32<<20) + } + count := int64(0) + for count < length { + blockSize := int64(len(zeroBuffer)) + remaining := length - count + if remaining < blockSize { + blockSize = remaining + } + written, err := outFile.Write(zeroBuffer[:blockSize]) + if err != nil { + return errors.Wrapf(err, "unable to write %d zeroes at offset %d: %v", length, start+count, err) + } + count += int64(written) + } + return nil +} + +func StreamDataToFile(r io.Reader, fileName string, preallocate bool) (int64, int64, error) { + var outFile *os.File + var bytesRead, bytesWritten int64 + outFile, err := OpenFileOrBlockDevice(fileName) + if err != nil { + return 0, 0, err + } + defer outFile.Close() + + if !preallocate { + var isDevice bool + zeroWriter := AppendZeroWithTruncate + isDevice, err = IsDevice(fileName) + if err != nil { + return 0, 0, err + } + + if isDevice { + zeroWriter = PunchHole + } + + bytesRead, bytesWritten, err = copyWithSparseCheck(outFile, r, zeroWriter) + } else { + bytesRead, err = io.Copy(outFile, r) + bytesWritten = bytesRead + } + + if err != nil { + os.Remove(outFile.Name()) + if strings.Contains(err.Error(), "no space left on device") { + err = errors.Wrapf(err, "unable to write to file") + } + return bytesRead, bytesWritten, err + } + + klog.Infof("Read %d bytes, wrote %d bytes to %s", bytesRead, bytesWritten, outFile.Name()) + + err = outFile.Sync() + + return bytesRead, bytesWritten, err +} + +type zeroWriterFunc func(*os.File, int64, int64) error + +func copyWithSparseCheck(dst *os.File, src io.Reader, zeroWriter zeroWriterFunc) (int64, int64, error) { + klog.Infof("copyWithSparseCheck to %s", dst.Name()) + const buffSize = 32 * 1024 + var bytesRead, bytesWritten int64 + zeroBuf := make([]byte, buffSize) + writeBuf := make([]byte, buffSize) + var writeOffset int64 + for { + nr, er := src.Read(writeBuf) + if nr > 0 { + var nw int + var ew error + if bytes.Equal(writeBuf[0:nr], zeroBuf[0:nr]) { + bytesRead += int64(nr) + } else { + if bytesRead > writeOffset { + // zeroWriter func should seek to bytesRead before returning + ew = zeroWriter(dst, writeOffset, bytesRead-writeOffset) + if ew != nil { + klog.Errorf("Error zeroing range in destination file: %v", ew) + return bytesRead, bytesWritten, ew + } + } + nw, ew = dst.Write(writeBuf[0:nr]) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = fmt.Errorf("invalid write result") + } + } + bytesRead += int64(nr) + bytesWritten += int64(nw) + writeOffset = bytesRead + if ew != nil { + return bytesRead, bytesWritten, ew + } + if nr != nw { + return bytesRead, bytesWritten, io.ErrShortWrite + } + } + } + if er != nil { + if er != io.EOF { + return bytesRead, bytesWritten, er + } + break + } + } + if bytesRead > writeOffset { + if err := zeroWriter(dst, writeOffset, bytesRead-writeOffset); err != nil { + klog.Errorf("Error zeroing range in destination file: %v", err) + return bytesRead, bytesWritten, err + } + } + return bytesRead, bytesWritten, nil +} diff --git a/images/cdi-artifact/pkg/util/file_format.go b/images/cdi-artifact/pkg/util/file_format.go new file mode 100644 index 0000000000..bede0753da --- /dev/null +++ b/images/cdi-artifact/pkg/util/file_format.go @@ -0,0 +1,22 @@ +package util + +import "os" + +func GetFormat(path string) (string, error) { + const ( + formatQcow2 = "qcow2" + formatRaw = "raw" + ) + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return formatQcow2, nil + } + return "", err + } + mode := info.Mode() + if mode&os.ModeDevice != 0 { + return formatRaw, nil + } + return formatQcow2, nil +} diff --git a/images/cdi-artifact/pkg/util/prometheus/prometheus.go b/images/cdi-artifact/pkg/util/prometheus/prometheus.go new file mode 100644 index 0000000000..3e0797b11a --- /dev/null +++ b/images/cdi-artifact/pkg/util/prometheus/prometheus.go @@ -0,0 +1,128 @@ +package prometheus + +import ( + "fmt" + "io" + "net/http" + "os" + "path" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "k8s.io/client-go/util/cert" + "k8s.io/klog/v2" + + "kubevirt.io/containerized-data-importer/pkg/util" +) + +// ProgressReader is a counting reader that reports progress to prometheus. +type ProgressReader struct { + util.CountingReader + metric ProgressMetric + total uint64 + final bool +} + +type ProgressMetric interface { + Add(value float64) + Get() (float64, error) + Delete() +} + +// NewProgressReader creates a new instance of a prometheus updating progress reader. +func NewProgressReader(r io.ReadCloser, metric ProgressMetric, total uint64) *ProgressReader { + promReader := &ProgressReader{ + CountingReader: util.CountingReader{ + Reader: r, + Current: 0, + }, + metric: metric, + total: total, + final: true, + } + + return promReader +} + +// StartTimedUpdate starts the update timer to automatically update every second. +func (r *ProgressReader) StartTimedUpdate() { + // Start the progress update thread. + go r.timedUpdateProgress() +} + +func (r *ProgressReader) timedUpdateProgress() { + cont := true + for cont { + // Update every second. + time.Sleep(time.Second) + cont = r.updateProgress() + } +} + +func (r *ProgressReader) updateProgress() bool { + if r.total > 0 { + finished := r.final && r.Done + currentProgress := 100.0 + if !finished && r.Current < r.total { + currentProgress = float64(r.Current) / float64(r.total) * 100.0 + } + progress, err := r.metric.Get() + if err != nil { + klog.Errorf("updateProgress: failed to read metric; %v", err) + return true // true ==> to try again // todo - how to avoid endless loop in case it's a constant error? + } + if currentProgress > progress { + r.metric.Add(currentProgress - progress) + } + klog.V(1).Infoln(fmt.Sprintf("%.2f", currentProgress)) + return !finished + } + return false +} + +// SetNextReader replaces the current counting reader with a new one, +// for tracking progress over multiple readers. +func (r *ProgressReader) SetNextReader(reader io.ReadCloser, final bool) { + r.CountingReader = util.CountingReader{ + Reader: reader, + Current: r.Current, + Done: false, + } + r.final = final +} + +// StartPrometheusEndpoint starts an http server providing a prometheus endpoint using the passed +// in directory to store the self signed certificates that will be generated before starting the +// http server. +func StartPrometheusEndpoint(certsDirectory string) { + certBytes, keyBytes, err := cert.GenerateSelfSignedCertKey("cloner_target", nil, nil) + if err != nil { + klog.Error("Error generating cert for prometheus") + return + } + + certFile := path.Join(certsDirectory, "tls.crt") + if err = os.WriteFile(certFile, certBytes, 0600); err != nil { + klog.Error("Error writing cert file") + return + } + + keyFile := path.Join(certsDirectory, "tls.key") + if err = os.WriteFile(keyFile, keyBytes, 0600); err != nil { + klog.Error("Error writing key file") + return + } + + go func() { + server := &http.Server{ + Addr: ":8443", + ReadHeaderTimeout: 10 * time.Second, + Handler: promhttp.Handler(), + } + + if err := server.ListenAndServeTLS(certFile, keyFile); err != nil { + return + } + }() +} diff --git a/images/cdi-artifact/pkg/util/util.go b/images/cdi-artifact/pkg/util/util.go new file mode 100644 index 0000000000..87f830c630 --- /dev/null +++ b/images/cdi-artifact/pkg/util/util.go @@ -0,0 +1,105 @@ +package util + +import ( + "bufio" + "encoding/base64" + "io" + "math" + "os" + "strings" + + "github.com/pkg/errors" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + "kubevirt.io/containerized-data-importer/pkg/common" +) + +const ( + blockdevFileName = "/usr/sbin/blockdev" + // DefaultAlignBlockSize is the alignment size we use to align disk images, its a multiple of all known hardware block sizes 512/4k/8k/32k/64k. + DefaultAlignBlockSize = 1024 * 1024 +) + +// CountingReader is a reader that keeps track of how much has been read. +type CountingReader struct { + Reader io.ReadCloser + Current uint64 + Done bool +} + +// ParseEnvVar provides a wrapper to attempt to fetch the specified env var. +func ParseEnvVar(envVarName string, decode bool) (string, error) { + value := os.Getenv(envVarName) + if decode { + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", errors.Errorf("error decoding environment variable %q", envVarName) + } + value = string(v) + } + return value, nil +} + +func (r *CountingReader) Read(p []byte) (n int, err error) { + n, err = r.Reader.Read(p) + r.Current += uint64(n) + r.Done = errors.Is(err, io.EOF) + return n, err +} + +func (r *CountingReader) Close() error { + return r.Reader.Close() +} + +// GetAvailableSpaceByVolumeMode calls another method based on the volumeMode parameter to get the amount of available space. +func GetAvailableSpaceByVolumeMode(volumeMode v1.PersistentVolumeMode) (int64, error) { + if volumeMode == v1.PersistentVolumeBlock { + return GetAvailableSpaceBlock(common.WriteBlockPath) + } + return GetAvailableSpace(common.ImporterVolumePath) +} + +// MinQuantity calculates the minimum of two quantities. +func MinQuantity(availableSpace, imageSize *resource.Quantity) resource.Quantity { + if imageSize.Cmp(*availableSpace) == 1 { + return *availableSpace + } + return *imageSize +} + +// WriteTerminationMessage writes the passed in message to the default termination message file. +func WriteTerminationMessage(message string) error { + return WriteTerminationMessageToFile(common.PodTerminationMessageFile, message) +} + +// WriteTerminationMessageToFile writes the passed in message to the passed in message file. +func WriteTerminationMessageToFile(file, message string) error { + message = strings.ReplaceAll(message, "\n", " ") + scanner := bufio.NewScanner(strings.NewReader(message)) + + if scanner.Scan() { + if err := os.WriteFile(file, scanner.Bytes(), 0600); err != nil { + return errors.Wrap(err, "could not create termination message file") + } + } + return nil +} + +// RoundDown returns the number rounded down to the nearest multiple. +func RoundDown(number, multiple int64) int64 { + return number / multiple * multiple +} + +// RoundUp returns the number rounded up to the nearest multiple. +func RoundUp(number, multiple int64) int64 { + partitions := math.Ceil(float64(number) / float64(multiple)) + return int64(partitions) * multiple +} + +// GetUsableSpace calculates usable space to use taking file system overhead into account. +func GetUsableSpace(filesystemOverhead float64, availableSpace int64) int64 { + spaceWithOverhead := int64(math.Ceil((1 - filesystemOverhead) * float64(availableSpace))) + return RoundDown(spaceWithOverhead, DefaultAlignBlockSize) +} diff --git a/images/cdi-artifact/staging/src/github.com/docker/docker/api/types/versions/compare.go b/images/cdi-artifact/staging/src/github.com/docker/docker/api/types/versions/compare.go new file mode 100644 index 0000000000..a99e9ebfd1 --- /dev/null +++ b/images/cdi-artifact/staging/src/github.com/docker/docker/api/types/versions/compare.go @@ -0,0 +1,79 @@ +// Copyright 2017 The Docker Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package versions + +import ( + "strconv" + "strings" +) + +// compare compares two version strings +// returns -1 if v1 < v2, 1 if v1 > v2, 0 otherwise. +func compare(v1, v2 string) int { + if v1 == v2 { + return 0 + } + var ( + currTab = strings.Split(v1, ".") + otherTab = strings.Split(v2, ".") + ) + + maxVer := len(currTab) + if len(otherTab) > maxVer { + maxVer = len(otherTab) + } + for i := 0; i < maxVer; i++ { + var currInt, otherInt int + + if len(currTab) > i { + currInt, _ = strconv.Atoi(currTab[i]) + } + if len(otherTab) > i { + otherInt, _ = strconv.Atoi(otherTab[i]) + } + if currInt > otherInt { + return 1 + } + if otherInt > currInt { + return -1 + } + } + return 0 +} + +// LessThan checks if a version is less than another +func LessThan(v, other string) bool { + return compare(v, other) == -1 +} + +// LessThanOrEqualTo checks if a version is less than or equal to another +func LessThanOrEqualTo(v, other string) bool { + return compare(v, other) <= 0 +} + +// GreaterThan checks if a version is greater than another +func GreaterThan(v, other string) bool { + return compare(v, other) == 1 +} + +// GreaterThanOrEqualTo checks if a version is greater than or equal to another +func GreaterThanOrEqualTo(v, other string) bool { + return compare(v, other) >= 0 +} + +// Equal checks if a version is equal to another +func Equal(v, other string) bool { + return compare(v, other) == 0 +} diff --git a/images/cdi-artifact/staging/src/github.com/docker/docker/go.mod b/images/cdi-artifact/staging/src/github.com/docker/docker/go.mod new file mode 100644 index 0000000000..7126574947 --- /dev/null +++ b/images/cdi-artifact/staging/src/github.com/docker/docker/go.mod @@ -0,0 +1,3 @@ +module github.com/docker/docker + +go 1.24.0 diff --git a/images/cdi-artifact/staging/src/github.com/docker/docker/registry/registry.go b/images/cdi-artifact/staging/src/github.com/docker/docker/registry/registry.go new file mode 100644 index 0000000000..5c3672b0e7 --- /dev/null +++ b/images/cdi-artifact/staging/src/github.com/docker/docker/registry/registry.go @@ -0,0 +1,15 @@ +// Copyright 2015 The Docker Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry diff --git a/images/cdi-artifact/static_binaries/hello.c b/images/cdi-artifact/static_binaries/hello.c deleted file mode 100644 index 7b70a90577..0000000000 --- a/images/cdi-artifact/static_binaries/hello.c +++ /dev/null @@ -1,6 +0,0 @@ -#include - -int main() { - printf("hello cdi\n"); - return 0; -} \ No newline at end of file diff --git a/images/cdi-artifact/static_binaries/print_file_context.c b/images/cdi-artifact/static_binaries/print_file_context.c deleted file mode 100644 index 34947e4010..0000000000 --- a/images/cdi-artifact/static_binaries/print_file_context.c +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include - -int main(int argc, char *argv[]) { - FILE *fptr; - char myContent[100]; - // Check for correct command-line arguments - if (argc != 2) { - printf("Usage: %s \n", argv[0]); - return 1; - } - - fptr = fopen(argv[1], "r"); // Open in read mode - - if(fptr != NULL) { - // Read the content and print it - while (fgets(myContent,100,fptr)) { - printf("%s", myContent); - } - } else { - perror("Not able to open the file"); - fclose(fptr); - return 1; - } - - - fclose(fptr); // Close the file - return 0; -} \ No newline at end of file diff --git a/images/cdi-artifact/unpack-bundle.sh b/images/cdi-artifact/unpack-bundle.sh deleted file mode 100755 index 1e78812011..0000000000 --- a/images/cdi-artifact/unpack-bundle.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -# Copyright 2023 Flant JSC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -DIR=$1 - -cat "$DIR/manifest.json" | jq -r '.[].RepoTags[0]' | \ - while read image; do - (set -x; mkdir -p "$image") - cat "$DIR/manifest.json" | jq -r --arg tag "$image" '.[]| select(.RepoTags[0] == $tag).Layers[]' | \ - while read layer; do - (set -x; tar -C "$image" --overwrite --exclude='./var/run/*' -xf "$DIR/$layer" .) || true - done -done diff --git a/images/cdi-artifact/werf.inc.yaml b/images/cdi-artifact/werf.inc.yaml index c3e68bccba..1a725eaae5 100644 --- a/images/cdi-artifact/werf.inc.yaml +++ b/images/cdi-artifact/werf.inc.yaml @@ -1,13 +1,10 @@ --- -{{- $gitRepoName := "3p-containerized-data-importer" }} -{{- $gitRepoUrl := (printf "%s/%s" "deckhouse" $gitRepoName) }} -{{- $version := get $.Core $gitRepoName }} +{{- $version := "local" }} {{- $name := print .ImageName "-dependencies" -}} {{- define "$name" -}} binaries: - /cdi-binaries/cdi-importer -- /cdi-binaries/cdi-source-update-poller packages: - libnbd - libxml2 @@ -22,44 +19,27 @@ packages: {{ $builderDependencies := include "$name" . | fromYaml }} -image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact -final: false -fromImage: builder/src -git: - - add: {{ .ModuleDir }}/images/{{ .ImageName }} - to: /src - stageDependencies: - install: - - '**/*' - excludePaths: - - patches/README.md -secrets: -- id: SOURCE_REPO - value: {{ $.SOURCE_REPO }} -shell: - install: - - | - echo "Git clone {{ $gitRepoName }} repository..." - git clone --depth=1 --branch {{ $version }} $(cat /run/secrets/SOURCE_REPO)/{{ $gitRepoUrl }} /src/containerized-data-importer - - rm -rf /src/containerized-data-importer/.git - ---- image: {{ .ModuleNamePrefix }}{{ .ImageName }} final: false fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-alt-1.25" "builder/golang-alt-svace-1.25" }} mount: {{- include "mount points for golang builds" . }} secrets: -- id: SOURCE_REPO - value: {{ $.SOURCE_REPO }} - id: GOPROXY value: {{ .GOPROXY }} +git: +- add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src + stageDependencies: + install: + - go.mod + - go.sum + setup: + - '**/*' + excludePaths: + - .git + - werf.inc.yaml import: -- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact - add: /src/containerized-data-importer - to: /containerized-data-importer - before: install {{- include "importPackageImages" (list . $builderDependencies.packages "install") -}} shell: install: @@ -75,53 +55,16 @@ shell: export GOPROXY=$(cat /run/secrets/GOPROXY) echo Download Go modules. - cd /containerized-data-importer + cd /src go mod download - go mod vendor - - # Apply patch for json-patch from 3p-cdi repo - git apply --ignore-space-change --ignore-whitespace patches/replace-op-for-evanphx-json-patch-v5-lib.patch - setup: - mkdir /cdi-binaries - - cd /containerized-data-importer + - cd /src - export GOOS=linux - export GOARCH=amd64 - export CGO_ENABLED=0 - - export X_FLAGS="-X kubevirt.io/containerized-data-importer/pkg/version.gitVersion=v{{ $version }}-patched" - - - echo ============== Build cdi-apiserver =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-apiserver" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w $X_FLAGS" -o /cdi-binaries/cdi-apiserver ./cmd/cdi-apiserver`) | nindent 6 }} - - - echo ============== Build cdi-cloner =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-cloner" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-cloner ./cmd/cdi-cloner`) | nindent 6 }} - - - echo ============== Build cdi-controller =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-controller" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-controller ./cmd/cdi-controller`) | nindent 6 }} - - - echo ============== Build cdi-uploadproxy =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-uploadproxy" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-uploadproxy ./cmd/cdi-uploadproxy`) | nindent 6 }} - - - echo ============== Build cdi-image-size-detection =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-image-size-detection" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-image-size-detection ./tools/cdi-image-size-detection`) | nindent 6 }} - - - echo ============== Build cdi-operator =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-operator" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-operator ./cmd/cdi-operator`) | nindent 6 }} - - export CGO_ENABLED=1 - echo ============== Build cdi-importer =========== @@ -129,44 +72,5 @@ shell: {{- $_ := set $ "ProjectName" (list .ImageName "cdi-importer" | join "/") }} {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-importer ./cmd/cdi-importer`) | nindent 6 }} - - echo ============== Build cdi-source-update-poller =========== - - | - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-source-update-poller" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cdi-source-update-poller ./tools/cdi-source-update-poller`) | nindent 6 }} - - chown -R 64535:64535 /cdi-binaries/* - ls -la /cdi-binaries - ---- -image: {{ .ModuleNamePrefix }}{{ .ImageName }}-cbuilder -final: false -fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} -git: - - add: {{ .ModuleDir }}/images/{{ .ImageName }} - to: / - includePaths: - - static_binaries - stageDependencies: - install: - - '*.c' -shell: - install: -{{- if eq $.SVACE_ENABLED "false" }} - {{- include "debian packages proxy" . | nindent 2 }} - - apt-get install --yes musl-dev musl-tools - {{- include "debian packages clean" . | nindent 2 }} -{{- else }} - {{- include "alt packages proxy" . | nindent 2 }} - - apt-get -qq install -y musl-devel musl-devel-static - {{- include "alt packages clean" . | nindent 2 }} -{{- end }} - - | - cd /static_binaries - echo "Building simple app that prints hello cdi" - mkdir -p /bins - {{- $_ := set $ "ProjectName" (list .ImageName "hello" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `musl-gcc -static -Os -o /bins/hello hello.c`) | nindent 6 }} - {{- $_ := set $ "ProjectName" (list .ImageName "printFile" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `musl-gcc -static -Os -o /bins/printFile print_file_context.c`) | nindent 6 }} - strip /bins/hello - strip /bins/printFile diff --git a/images/cdi-cloner/werf.inc.yaml b/images/cdi-cloner/werf.inc.yaml deleted file mode 100644 index 76f1e49daf..0000000000 --- a/images/cdi-cloner/werf.inc.yaml +++ /dev/null @@ -1,74 +0,0 @@ ---- -image: {{ .ModuleNamePrefix }}{{ .ImageName }} -fromImage: {{ .ModuleNamePrefix }}distroless -git: - {{- include "image mount points" . }} -import: -- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-bins - add: /relocate - to: / - before: setup -imageSpec: - config: - entrypoint: ["/usr/bin/cloner-startup"] - user: 64535 ---- -{{- $name := print .ImageName "-dependencies" -}} -{{- define "$name" -}} -binaries: -- /usr/bin/cloner-startup -- /usr/bin/hello -- /usr/bin/cdi-cloner -{{- end -}} - -{{ $virtCDIClonerDependencies := include "$name" . | fromYaml }} - -image: {{ .ModuleNamePrefix }}{{ .ImageName }}-bins -final: false -fromImage: {{ .ModuleNamePrefix }}base-alt-p11-binaries -import: -- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-gobuild - add: /cdi-binaries - to: /usr/bin - includePaths: - - cloner-startup - before: install -- image: {{ .ModuleNamePrefix }}cdi-artifact-cbuilder - add: /bins - to: /usr/bin - before: install - includePaths: - - hello -- image: {{ .ModuleNamePrefix }}cdi-artifact - add: /cdi-binaries - to: /usr/bin - includePaths: - - cdi-cloner - before: install -# Source https://github.com/kubevirt/containerized-data-importer/blob/v1.60.3/cmd/cdi-cloner/BUILD.bazel -shell: - install: - - | - /relocate_binaries.sh -i "{{ $virtCDIClonerDependencies.binaries | join " " }}" -o /relocate ---- -image: {{ .ModuleNamePrefix }}{{ .ImageName }}-gobuild -final: false -fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} -git: - - add: {{ .ModuleDir }}/images/{{ .ImageName }}/cloner-startup - to: /app - stageDependencies: - install: - - '**/*' -secrets: -- id: GOPROXY - value: {{ .GOPROXY }} -shell: - install: - - | - mkdir -p /cdi-binaries - cd /app - export GOPROXY=$(cat /run/secrets/GOPROXY) - go mod download - {{- $_ := set $ "ProjectName" (list .ImageName "cdi-cloner" | join "/") }} - {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" -o /cdi-binaries/cloner-startup ./cmd/cloner-startup`) | nindent 6 }} diff --git a/images/cdi-controller/mount-points.yaml b/images/cdi-controller/mount-points.yaml deleted file mode 100644 index d68ce54296..0000000000 --- a/images/cdi-controller/mount-points.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# A list of pre-created mount points for containerd strict mode. -# -# Some volume mounts are ignored: -# - /tmp - already in the 'distroless' base image. - -dirs: - # Create dirs in /run, as /var/run is a symlink to /run. - - /run/cdi/token/keys - - /run/certs/cdi-uploadserver-signer - - /run/certs/cdi-uploadserver-client-signer - - /run/ca-bundle/cdi-uploadserver-signer-bundle - - /run/ca-bundle/cdi-uploadserver-client-signer-bundle - - /kubeconfig.local diff --git a/images/cdi-controller/werf.inc.yaml b/images/cdi-controller/werf.inc.yaml deleted file mode 100644 index 814dde77fa..0000000000 --- a/images/cdi-controller/werf.inc.yaml +++ /dev/null @@ -1,61 +0,0 @@ ---- -image: {{ .ModuleNamePrefix }}{{ .ImageName }} -fromImage: {{ .ModuleNamePrefix }}distroless -git: - {{- include "image mount points" . }} -import: -- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-bins - add: /relocate - to: / - before: setup -imageSpec: - config: - entrypoint: ["/usr/bin/cdi-controller", "-alsologtostderr"] - user: 64535 ---- -{{- define "cdi-controller-deps" -}} -binaries: - - /usr/bin/cdi-controller -packages: - - tar -{{- end -}} - -{{ $cdiClonerDependencies := include "cdi-controller-deps" . | fromYaml }} - -image: {{ .ModuleNamePrefix }}{{ .ImageName }}-bins -final: false -fromImage: {{ .ModuleNamePrefix }}base-alt-p11-binaries -import: -- image: tools/util-linux - add: / - to: /relocate/usr - after: setup - includePaths: - - sbin/blockdev -- image: {{ .ModuleNamePrefix }}cdi-artifact-cbuilder - add: /bins - to: /relocate/usr/bin - after: setup - includePaths: - - printFile -- image: {{ .ModuleNamePrefix }}cdi-artifact - add: /cdi-binaries - to: /usr/bin - includePaths: - - cdi-controller - before: setup -# Source https://github.com/kubevirt/containerized-data-importer/blob/v1.60.3/cmd/cdi-controller/BUILD.bazel -shell: - install: - {{- include "alt packages proxy" . | nindent 2 }} - - | - apt-get install --yes \ - {{ $cdiClonerDependencies.packages | join " " }} - {{- include "alt packages clean" . | nindent 2 }} - setup: - - /relocate_binaries.sh -i "{{ $cdiClonerDependencies.binaries | join " " }}" -o /relocate -# tmp folder need for ready file -# https://github.com/kubevirt/containerized-data-importer/blob/v1.60.3/pkg/operator/resources/namespaced/controller.go#L243 - - | - mkdir -p /relocate/{tmp,var/run/certs/cdi-uploadserver-signer,var/run/certs/cdi-uploadserver-client-signer} - chown -R 64535:64535 /relocate/ diff --git a/images/cdi-importer/mount-points.yaml b/images/cdi-importer/mount-points.yaml index f926961f28..8203798884 100644 --- a/images/cdi-importer/mount-points.yaml +++ b/images/cdi-importer/mount-points.yaml @@ -1,6 +1,6 @@ # A list of pre-created mount points for containerd strict mode. # -# See https://github.com/deckhouse/3p-containerized-data-importer/blob/d5fa5124b8a645521843814fffecdf385b74b379/pkg/controller/import-controller.go#L962 +# See images/cdi-artifact/pkg/controller/import-controller.go. # # Some volume mounts are ignored: # - /extraheaders - Etra headers not implemented in virtualization-controller. @@ -14,4 +14,3 @@ dirs: - /proxycerts - /scratch - /shared - diff --git a/images/cdi-importer/werf.inc.yaml b/images/cdi-importer/werf.inc.yaml index 6ae9a3ecb6..24b121aa8c 100644 --- a/images/cdi-importer/werf.inc.yaml +++ b/images/cdi-importer/werf.inc.yaml @@ -8,7 +8,7 @@ import: add: /relocate to: / before: setup -# Source https://github.com/kubevirt/containerized-data-importer/blob/v1.60.3/cmd/cdi-importer/BUILD.bazel +# Binaries are built from images/cdi-artifact by images/cdi-artifact/werf.inc.yaml. imageSpec: config: entrypoint: ["/usr/bin/cdi-importer", "-alsologtostderr"] @@ -22,7 +22,7 @@ binaries: - /usr/lib64/nbdkit/filters/*.so - /usr/lib64/nbdkit/plugins/*.so # CDI binaries - - /usr/bin/cdi-image-size-detection /usr/bin/cdi-importer /usr/bin/cdi-source-update-poller + - /usr/bin/cdi-importer # QEMU bins - /usr/bin/qemu-img # do not include glibc, it will be replaced by one from qemu-img @@ -59,9 +59,7 @@ import: to: /usr/bin before: setup includePaths: - - cdi-image-size-detection - cdi-importer - - cdi-source-update-poller - image: {{ .ModuleNamePrefix }}qemu add: /qemu-img to: /qemu-img diff --git a/images/cdi-operator/mount-points.yaml b/images/cdi-operator/mount-points.yaml deleted file mode 100644 index 624df72961..0000000000 --- a/images/cdi-operator/mount-points.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# A list of pre-created mount points for containerd strict mode. - -dirs: - - /kubeconfig.local diff --git a/images/cdi-operator/werf.inc.yaml b/images/cdi-operator/werf.inc.yaml deleted file mode 100644 index c720c33d50..0000000000 --- a/images/cdi-operator/werf.inc.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -image: {{ .ModuleNamePrefix }}{{ .ImageName }} -fromImage: {{ .ModuleNamePrefix }}distroless -git: - {{- include "image mount points" . }} -import: -- image: {{ .ModuleNamePrefix }}cdi-artifact - add: /cdi-binaries - to: /usr/bin - includePaths: - - cdi-operator - before: setup -imageSpec: - config: - entrypoint: ["/usr/bin/cdi-operator"] - user: 64535 diff --git a/images/kube-api-rewriter/STRUCTURE.md b/images/kube-api-rewriter/STRUCTURE.md index bdbf4a2d52..95f788aa92 100644 --- a/images/kube-api-rewriter/STRUCTURE.md +++ b/images/kube-api-rewriter/STRUCTURE.md @@ -123,49 +123,6 @@ flowchart virt-api-proxy <----> kube-api virt-handler-proxy <----> kube-api - subgraph cdi ["CDI"] - subgraph cdi-operator-deploy ["`Deploy/cdi-operator`"] - cdi-operator-proxy{{"container: - proxy"}} - cdi-operator("`container: - virt-handler`") - cdi-operator --> cdi-operator-proxy - cdi-operator-proxy --> cdi-operator - end - - subgraph cdi-deployment-deploy ["`Deploy/cdi-deployment`"] - cdi-deployment-proxy{{"container: - proxy"}} - cdi-deployment("`container: - cdi-eployment`") - cdi-deployment --> cdi-deployment-proxy - cdi-deployment-proxy --> cdi-deployment - end - - subgraph cdi-api-deploy ["`Deploy/cdi-api`"] - cdi-api-proxy{{"container: - proxy"}} - cdi-api("`container: - cdi-api`") - cdi-api --> cdi-api-proxy - cdi-api-proxy --> cdi-api - end - - subgraph cdi-exportproxy-deploy ["`Deploy/cdi-exportproxy`"] - cdi-exportproxy-proxy{{"container: - proxy"}} - cdi-exportproxy("`container: - cdi-exportproxy`") - cdi-exportproxy --> cdi-exportproxy-proxy - cdi-exportproxy-proxy --> cdi-exportproxy - end - end - kube-api <----> cdi-operator-proxy - kube-api <----> cdi-deployment-proxy - kube-api <----> cdi-api-proxy - kube-api <----> cdi-exportproxy-proxy - - subgraph d8virt ["D8 API"] subgraph d8-virt-deploy ["Deploy/virtualization-controller"] d8-virt-controller-proxy("`container: @@ -223,31 +180,13 @@ block-beta virtexportproxyproxy --> kubeapiserver space:5 - cdioperatorproxy --> kubeapiserver - cdiapiproxy --> kubeapiserver - cdideploymentproxy --> kubeapiserver - cdiuploadproxyproxy --> kubeapiserver virtualizationcontrollerproxy --> kubeapiserver - %% Proxies in CDI Pods. - cdioperatorproxy(["proxy"]) - cdiapiproxy(["proxy"]) - cdideploymentproxy(["proxy"]) - cdiuploadproxyproxy(["proxy"]) virtualizationcontrollerproxy(["proxy"]) - %% Links inside CDI Pods. space:5 - cdioperator --> cdioperatorproxy - cdiapi--> cdiapiproxy - cdideployment --> cdideploymentproxy - cdiuploadproxy --> cdiuploadproxyproxy virtualizationcontroller --> virtualizationcontrollerproxy - cdioperator["cdi-operator"] - cdiapi["cdi-api"] - cdideployment["cdi-deployment"] - cdiuploadproxy["cdi-uploadproxy"] virtualizationcontroller["virtualization- controller"] ``` @@ -263,7 +202,7 @@ block-beta ``` - Add a volume and a volumeMount to pass new kubeconfig as file to the main container. - Set KUBECONFIG variable in the main container. File should contain configuration to connect to proxy port. - - Note: kubevirt containers use --kubeconfig flag, cdi containers use KUBECONFIG env variable. + - Note: KubeVirt containers use --kubeconfig flag. - Add a new sidecar container with the proxy. - Set WEBHOOK_ADDRESS if webhook proxying is required. - Add volumeMount with a certificate and set WEBHOOK_CERT_FILE and WEBHOOK_KEY_FILE to use the certificate. diff --git a/images/kube-api-rewriter/mount-points.yaml b/images/kube-api-rewriter/mount-points.yaml index fa5ef6daed..9288d9eb42 100644 --- a/images/kube-api-rewriter/mount-points.yaml +++ b/images/kube-api-rewriter/mount-points.yaml @@ -3,5 +3,3 @@ dirs: - /etc/virt-operator/certificates - /etc/virt-api/certificates - # Create dirs in /run, as /var/run is a symlink to /run. - - /run/certs/cdi-apiserver-server-cert diff --git a/images/virtual-disk-importer/werf.inc.yaml b/images/virtual-disk-importer/werf.inc.yaml new file mode 100644 index 0000000000..e1cb90c3e9 --- /dev/null +++ b/images/virtual-disk-importer/werf.inc.yaml @@ -0,0 +1,7 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +fromImage: {{ .ModuleNamePrefix }}cdi-importer +imageSpec: + config: + entrypoint: ["/usr/bin/cdi-importer", "-alsologtostderr"] + user: 64535 diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index f134d49e3a..5edcca89a8 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -50,6 +50,7 @@ import ( mcapi "github.com/deckhouse/virtualization-controller/pkg/controller/moduleconfig/api" "github.com/deckhouse/virtualization-controller/pkg/controller/nodeusbdevice" "github.com/deckhouse/virtualization-controller/pkg/controller/resourceslice" + "github.com/deckhouse/virtualization-controller/pkg/controller/storageprofile" "github.com/deckhouse/virtualization-controller/pkg/controller/usbdevice" "github.com/deckhouse/virtualization-controller/pkg/controller/vd" "github.com/deckhouse/virtualization-controller/pkg/controller/vdsnapshot" @@ -115,7 +116,6 @@ func main() { } var logDebugControllerList []string - fmt.Print(len(logDebugControllerList)) logDebugControllerListRaw := os.Getenv(logDebugControllerListEnv) if logDebugControllerListRaw != "" { logDebugControllerListRaw = strings.ReplaceAll(logDebugControllerListRaw, " ", "") @@ -348,13 +348,19 @@ func main() { } vdLogger := logger.NewControllerLogger(vd.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if _, err = vd.NewController(ctx, mgr, vdLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, vdStorageClassSettings); err != nil { + if _, err = vd.NewController(ctx, mgr, vdLogger, importSettings.ImporterImage, importSettings.DiskImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, vdStorageClassSettings); err != nil { log.Error(err.Error()) os.Exit(1) } viLogger := logger.NewControllerLogger(vi.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if _, err = vi.NewController(ctx, mgr, viLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.BounderImage, importSettings.Requirements, dvcrSettings, viStorageClassSettings); err != nil { + if _, err = vi.NewController(ctx, mgr, viLogger, importSettings.ImporterImage, importSettings.DiskImporterImage, importSettings.UploaderImage, importSettings.BounderImage, importSettings.Requirements, dvcrSettings, viStorageClassSettings); err != nil { + log.Error(err.Error()) + os.Exit(1) + } + + storageProfileLogger := logger.NewControllerLogger(storageprofile.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) + if _, err = storageprofile.NewController(mgr, storageProfileLogger); err != nil { log.Error(err.Error()) os.Exit(1) } diff --git a/images/virtualization-artifact/pkg/common/annotations/annotations.go b/images/virtualization-artifact/pkg/common/annotations/annotations.go index a487cab2b2..7530ce39dc 100644 --- a/images/virtualization-artifact/pkg/common/annotations/annotations.go +++ b/images/virtualization-artifact/pkg/common/annotations/annotations.go @@ -55,7 +55,7 @@ const ( AnnTolerationsHash = AnnAPIGroup + "/tolerations-hash" // AnnProvisionerTolerations provides a const for tolerations to use for provisioners. AnnProvisionerTolerations = AnnAPIGroup + "/provisioner-tolerations" - // AnnProvisionerName provides a name of data volume provisioner. + // AnnProvisionerName provides a name of the PVC provisioner. AnnProvisionerName = AnnAPIGroup + "/provisioner-name" // AnnDefaultStorageClass is the annotation indicating that a storage class is the default one. @@ -151,6 +151,31 @@ const ( // AnnDataExportRequest is the annotation for indicating that export requested. AnnDataExportRequest = "storage.deckhouse.io/data-export-request" + // PVC import annotations. + // These are set on the target PVC and on the importer Pod that copies data + // into the PVC (from DVCR or from another PVC during a clone). + // + // AnnPVCImportPhase tracks the lifecycle phase of the PVC import. + AnnPVCImportPhase = AnnAPIGroupV + "/pvc-import.phase" + // AnnPVCImportPod is set to the importer Pod name on the target PVC. + AnnPVCImportPod = AnnAPIGroupV + "/pvc-import.pod" + // AnnPVCImportSource indicates the import source type ("registry" or "pvc"). + AnnPVCImportSource = AnnAPIGroupV + "/pvc-import.source" + // AnnPVCImportEndpoint is the source endpoint (DVCR URL or sourceNS/sourceName). + AnnPVCImportEndpoint = AnnAPIGroupV + "/pvc-import.endpoint" + // AnnPVCImportSecret is the name of the auth secret to access the source. + AnnPVCImportSecret = AnnAPIGroupV + "/pvc-import.secret" + // AnnPVCImportCertConfigMap is the name of the CA bundle ConfigMap. + AnnPVCImportCertConfigMap = AnnAPIGroupV + "/pvc-import.cert-config-map" + // AnnPVCImportImageSize is the requested image size on the target PVC. + AnnPVCImportImageSize = AnnAPIGroupV + "/pvc-import.image-size" + // AnnPVCImportCreatedBy is set to "yes" on importer Pods created by the controller. + AnnPVCImportCreatedBy = AnnAPIGroupV + "/pvc-import.created-by" + // AnnPVCImportCloneStrategy stores the strategy used for PVC-to-PVC cloning. + AnnPVCImportCloneStrategy = AnnAPIGroupV + "/pvc-import.clone-strategy" + // AnnPVCImportCloneSnapshot stores the name of the VolumeSnapshot used for snapshot-based cloning. + AnnPVCImportCloneSnapshot = AnnAPIGroupV + "/pvc-import.clone-snapshot" + // TODO: remove deprecated annotations in the v1 version. // AnnStorageClassName is the annotation for indicating that storage class name. (USED IN STORAGE sds controllers) AnnStorageClassName = AnnAPIGroupV + "/storage-class-name" diff --git a/images/virtualization-artifact/pkg/common/consts.go b/images/virtualization-artifact/pkg/common/consts.go index 41ddc972c7..06d96da36e 100644 --- a/images/virtualization-artifact/pkg/common/consts.go +++ b/images/virtualization-artifact/pkg/common/consts.go @@ -34,6 +34,8 @@ const ( UploaderPort = 80 // ImporterPodImageNameVar is a name of variable with the image name for the importer Pod ImporterPodImageNameVar = "IMPORTER_IMAGE" + // DiskImporterPodImageNameVar is a name of variable with the image name for the VirtualDisk CDI importer Pod. + DiskImporterPodImageNameVar = "DISK_IMPORTER_IMAGE" // UploaderPodImageNameVar is a name of variable with the image name for the uploader Pod UploaderPodImageNameVar = "UPLOADER_IMAGE" // BounderPodImageNameVar is a name of variable with the image name for the bounder Pod diff --git a/images/virtualization-artifact/pkg/common/datavolume/importer.go b/images/virtualization-artifact/pkg/common/datavolume/importer.go deleted file mode 100644 index f93dc755b3..0000000000 --- a/images/virtualization-artifact/pkg/common/datavolume/importer.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2024 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package datavolume - -import "k8s.io/apimachinery/pkg/types" - -const importerPrimePrefix = "importer-prime-" - -func GetImporterPrimeName(pvcUID types.UID) string { - return importerPrimePrefix + string(pvcUID) -} diff --git a/images/virtualization-artifact/pkg/common/datavolume/util.go b/images/virtualization-artifact/pkg/common/datavolume/util.go deleted file mode 100644 index e23c6005b6..0000000000 --- a/images/virtualization-artifact/pkg/common/datavolume/util.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2024 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package datavolume - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - cdiv1beta1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" -) - -const DataVolumeKind = "DataVolume" - -// MakeOwnerReference makes controller owner reference for a DataVolume object. -// NOTE: GetObjectKind resets after creation, hence this method with hardcoded -// GVK as a workaround. -func MakeOwnerReference(dv *cdiv1beta1.DataVolume) metav1.OwnerReference { - gvk := schema.GroupVersionKind{ - Group: cdiv1beta1.SchemeGroupVersion.Group, - Version: cdiv1beta1.SchemeGroupVersion.Version, - Kind: DataVolumeKind, - } - return *metav1.NewControllerRef(dv, gvk) -} diff --git a/images/virtualization-artifact/pkg/common/network_policy/network_policy.go b/images/virtualization-artifact/pkg/common/network_policy/network_policy.go index 702df20d24..a847ab76b0 100644 --- a/images/virtualization-artifact/pkg/common/network_policy/network_policy.go +++ b/images/virtualization-artifact/pkg/common/network_policy/network_policy.go @@ -29,7 +29,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" ) -func CreateNetworkPolicy(ctx context.Context, c client.Client, obj metav1.Object, sup supplements.DataVolumeSupplement, finalizer string) error { +func CreateNetworkPolicy(ctx context.Context, c client.Client, obj metav1.Object, sup supplements.Generator, finalizer string) error { npName := sup.NetworkPolicy() networkPolicy := netv1.NetworkPolicy{ TypeMeta: metav1.TypeMeta{ diff --git a/images/virtualization-artifact/pkg/config/load_import_settings.go b/images/virtualization-artifact/pkg/config/load_import_settings.go index f05781ab2b..0130461c33 100644 --- a/images/virtualization-artifact/pkg/config/load_import_settings.go +++ b/images/virtualization-artifact/pkg/config/load_import_settings.go @@ -32,10 +32,11 @@ const ( ) type ImportSettings struct { - ImporterImage string - UploaderImage string - BounderImage string - Requirements corev1.ResourceRequirements + ImporterImage string + DiskImporterImage string + UploaderImage string + BounderImage string + Requirements corev1.ResourceRequirements } func LoadImportSettingsFromEnv() (ImportSettings, error) { @@ -46,6 +47,11 @@ func LoadImportSettingsFromEnv() (ImportSettings, error) { return ImportSettings{}, err } + settings.DiskImporterImage, err = GetRequiredEnvVar(common.DiskImporterPodImageNameVar) + if err != nil { + return ImportSettings{}, err + } + settings.UploaderImage, err = GetRequiredEnvVar(common.UploaderPodImageNameVar) if err != nil { return ImportSettings{}, err diff --git a/images/virtualization-artifact/pkg/controller/conditions/getter.go b/images/virtualization-artifact/pkg/controller/conditions/getter.go index c17ba47b68..5a46bb61c3 100644 --- a/images/virtualization-artifact/pkg/controller/conditions/getter.go +++ b/images/virtualization-artifact/pkg/controller/conditions/getter.go @@ -19,7 +19,6 @@ package conditions import ( corev1 "k8s.io/api/core/v1" virtv1 "kubevirt.io/api/core/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" ) func GetPodCondition(condType corev1.PodConditionType, conds []corev1.PodCondition) (corev1.PodCondition, bool) { @@ -32,23 +31,6 @@ func GetPodCondition(condType corev1.PodConditionType, conds []corev1.PodConditi return corev1.PodCondition{}, false } -const ( - DVRunningConditionType cdiv1.DataVolumeConditionType = "Running" - DVRunningConditionPendingReason string = "Pending" - DVQoutaNotExceededConditionType cdiv1.DataVolumeConditionType = "QuotaNotExceeded" - DVImagePullFailedReason string = "ImagePullFailed" -) - -func GetDataVolumeCondition(condType cdiv1.DataVolumeConditionType, conds []cdiv1.DataVolumeCondition) (cdiv1.DataVolumeCondition, bool) { - for _, cond := range conds { - if cond.Type == condType { - return cond, true - } - } - - return cdiv1.DataVolumeCondition{}, false -} - func GetKVVMCondition(condType virtv1.VirtualMachineConditionType, conds []virtv1.VirtualMachineCondition) (virtv1.VirtualMachineCondition, bool) { for _, cond := range conds { if cond.Type == condType { diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/dv.go b/images/virtualization-artifact/pkg/controller/kvbuilder/dv.go deleted file mode 100644 index b8ec43d433..0000000000 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/dv.go +++ /dev/null @@ -1,128 +0,0 @@ -/* -Copyright 2024 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kvbuilder - -import ( - "encoding/json" - "fmt" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - - "github.com/deckhouse/virtualization-controller/pkg/common" - "github.com/deckhouse/virtualization-controller/pkg/common/annotations" - "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" - "github.com/deckhouse/virtualization-controller/pkg/common/pvc" - "github.com/deckhouse/virtualization-controller/pkg/common/resource_builder" -) - -// DV is a helper to construct DataVolume to import an image from DVCR onto PVC. -type DV struct { - resource_builder.ResourceBuilder[*cdiv1.DataVolume] -} - -func NewDV(name types.NamespacedName) *DV { - return &DV{ - ResourceBuilder: resource_builder.NewResourceBuilder( - &cdiv1.DataVolume{ - TypeMeta: metav1.TypeMeta{ - Kind: "DataVolume", - APIVersion: cdiv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: name.Namespace, - Name: name.Name, - Annotations: map[string]string{ - "cdi.kubevirt.io/storage.deleteAfterCompletion": "false", - }, - }, - Spec: cdiv1.DataVolumeSpec{ - Source: &cdiv1.DataVolumeSource{}, - }, - }, resource_builder.ResourceBuilderOptions{}, - ), - } -} - -func (b *DV) SetPVC(storageClassName *string, - size resource.Quantity, - accessMode corev1.PersistentVolumeAccessMode, - volumeMode corev1.PersistentVolumeMode, -) { - b.Resource.Spec.PVC = pvc.CreateSpec(storageClassName, - size, - accessMode, - volumeMode, - ) -} - -func (b *DV) SetImmediate() { - b.AddAnnotation("cdi.kubevirt.io/storage.bind.immediate.requested", "true") -} - -func (b *DV) SetDataSource(source *cdiv1.DataVolumeSource) { - b.Resource.Spec.Source = source -} - -func (b *DV) SetNodePlacement(nodePlacement *provisioner.NodePlacement) error { - if nodePlacement == nil || len(nodePlacement.Tolerations) == 0 { - return nil - } - - anno := b.Resource.GetAnnotations() - if anno == nil { - anno = make(map[string]string) - } - - data, err := json.Marshal(nodePlacement.Tolerations) - if err != nil { - return fmt.Errorf("failed to marshal tolerations: %w", err) - } - - anno[annotations.AnnProvisionerTolerations] = string(data) - - err = provisioner.KeepNodePlacementTolerations(nodePlacement, b.Resource) - if err != nil { - return fmt.Errorf("failed to keep node placement: %w", err) - } - - return nil -} - -func (b *DV) SetRegistryDataSource(imageName, authSecret, caBundleConfigMap string) { - url := common.DockerRegistrySchemePrefix + imageName - - dataSource := cdiv1.DataVolumeSourceRegistry{ - URL: &url, - } - - if authSecret != "" { - dataSource.SecretRef = &authSecret - } - if caBundleConfigMap != "" { - dataSource.CertConfigMap = &caBundleConfigMap - } - - b.Resource.Spec.Source.Registry = &dataSource -} - -func (b *DV) SetBlankDataSource() { - b.Resource.Spec.Source.Blank = &cdiv1.DataVolumeBlankImage{} -} diff --git a/images/virtualization-artifact/pkg/controller/monitoring/progress.go b/images/virtualization-artifact/pkg/controller/monitoring/progress.go index c908257db3..4f8ed8e182 100644 --- a/images/virtualization-artifact/pkg/controller/monitoring/progress.go +++ b/images/virtualization-artifact/pkg/controller/monitoring/progress.go @@ -57,17 +57,32 @@ func GetImportProgressFromPod(ownerUID string, pod *corev1.Pod) (*ImportProgress return extractProgress(progressReport, ownerUID) } -// extractProgress parses final report and extracts metrics: -// registry_progress{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 47.68095477934807 -// registry_current_speed{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 2.12e+06 -// registry_average_speed{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 2.3832862149406234e+06 +// extractProgress parses the final report and extracts metrics. Two metric +// families are recognized: +// +// - registry_progress / registry_current_speed / registry_average_speed are +// emitted by dvcr-importer / dvcr-uploader pods (the "first half" import +// into DVCR for HTTP / Registry / Upload data sources). +// - kubevirt_cdi_import_progress_total is emitted by the cdi-importer pod +// (the "second half" import from DVCR into the target PVC; for ObjectRef +// CVI / VI it is also the only import pod). +// +// Example lines: +// +// registry_progress{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 47.6809 +// registry_current_speed{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 2.12e+06 +// registry_average_speed{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 2.38e+06 +// kubevirt_cdi_import_progress_total{ownerUID="b856691e-1038-11e9-a5ab-525500d15501"} 73.42 func extractProgress(report, ownerUID string) (*ImportProgress, error) { if report == "" { return nil, nil } // Note: invalid float format will be checked later using ParseFloat. - progressRe := regexp.MustCompile(`registry_progress\{ownerUID\="` + ownerUID + `"\} ([0-9e\+\-\.]+)`) + // Match either the dvcr-importer's registry_progress or the cdi-importer's + // kubevirt_cdi_import_progress_total metric. Both are reported in the same + // 0..100 scale, so either value is a valid pod-local progress percentage. + progressRe := regexp.MustCompile(`(?:registry_progress|kubevirt_cdi_import_progress_total)\{ownerUID\="` + ownerUID + `"\} ([0-9e\+\-\.]+)`) avgSpeedRe := regexp.MustCompile(`registry_average_speed\{ownerUID\="` + ownerUID + `"\} ([0-9e\+\-\.]+)`) curSpeedRe := regexp.MustCompile(`registry_current_speed\{ownerUID\="` + ownerUID + `"\} ([0-9e\+\-\.]+)`) @@ -78,7 +93,7 @@ func extractProgress(report, ownerUID string) (*ImportProgress, error) { raw := match[1] val, err := strconv.ParseFloat(raw, 64) if err != nil { - return nil, fmt.Errorf("parse registry_progress metric: %w", err) + return nil, fmt.Errorf("parse import progress metric: %w", err) } res.progress = val } diff --git a/images/virtualization-artifact/pkg/controller/monitoring/progress_test.go b/images/virtualization-artifact/pkg/controller/monitoring/progress_test.go index 04a449e381..8647a4fe68 100644 --- a/images/virtualization-artifact/pkg/controller/monitoring/progress_test.go +++ b/images/virtualization-artifact/pkg/controller/monitoring/progress_test.go @@ -34,3 +34,50 @@ registry_current_speed{ownerUID="11"} 2.345632345432e+6 t.Fatalf("%s is not expected human readable value for raw value %v", p.CurSpeed(), p.CurSpeedRaw()) } } + +// Test_ExtractProgress_CDIImporter verifies the parser accepts the +// cdi-importer's progress metric name and returns the value as the pod-local +// progress. cdi-importer does not emit registry_*_speed series, so download +// speed remains zero in this case. +func Test_ExtractProgress_CDIImporter(t *testing.T) { + p, err := extractProgress(` +kubevirt_cdi_import_progress_total{ownerUID="22"} 73.42 +`, `22`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p == nil { + t.Fatalf("expected progress, got nil") + } + if p.ProgressRaw() != 73.42 { + t.Fatalf("expected 73.42, got %v", p.ProgressRaw()) + } + if p.AvgSpeedRaw() != 0 || p.CurSpeedRaw() != 0 { + t.Fatalf("expected zero speed (cdi-importer does not emit speed), got avg=%d cur=%d", p.AvgSpeedRaw(), p.CurSpeedRaw()) + } +} + +// Test_ExtractProgress_DVCRTakesPrecedence covers the case where both metric +// families happen to be present on the same scrape (e.g. mixed report from a +// previous run). The dvcr-importer's registry_progress name is listed first in +// the alternation, so it wins; speeds from the same family are picked up too. +func Test_ExtractProgress_DVCRTakesPrecedence(t *testing.T) { + p, err := extractProgress(` +registry_progress{ownerUID="33"} 47.6 +registry_average_speed{ownerUID="33"} 1.0e+6 +registry_current_speed{ownerUID="33"} 2.0e+6 +kubevirt_cdi_import_progress_total{ownerUID="33"} 99.0 +`, `33`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p == nil { + t.Fatalf("expected progress, got nil") + } + if p.ProgressRaw() != 47.6 { + t.Fatalf("expected 47.6, got %v", p.ProgressRaw()) + } + if p.AvgSpeedRaw() != 1_000_000 || p.CurSpeedRaw() != 2_000_000 { + t.Fatalf("expected avg=1e6 cur=2e6, got avg=%d cur=%d", p.AvgSpeedRaw(), p.CurSpeedRaw()) + } +} diff --git a/images/virtualization-artifact/pkg/controller/service/condition.go b/images/virtualization-artifact/pkg/controller/service/condition.go index 6cc581e545..089a0272bc 100644 --- a/images/virtualization-artifact/pkg/controller/service/condition.go +++ b/images/virtualization-artifact/pkg/controller/service/condition.go @@ -21,7 +21,6 @@ import ( corev1 "k8s.io/api/core/v1" virtv1 "kubevirt.io/api/core/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" ) func CapitalizeFirstLetter(s string) string { @@ -36,15 +35,6 @@ func CapitalizeFirstLetter(s string) string { return string(runes) } -func GetDataVolumeCondition(conditionType cdiv1.DataVolumeConditionType, conditions []cdiv1.DataVolumeCondition) *cdiv1.DataVolumeCondition { - for i, condition := range conditions { - if condition.Type == conditionType { - return &conditions[i] - } - } - return nil -} - func GetPersistentVolumeClaimCondition(conditionType corev1.PersistentVolumeClaimConditionType, conditions []corev1.PersistentVolumeClaimCondition) *corev1.PersistentVolumeClaimCondition { for i, condition := range conditions { if condition.Type == conditionType { diff --git a/images/virtualization-artifact/pkg/controller/service/disk_import_service.go b/images/virtualization-artifact/pkg/controller/service/disk_import_service.go new file mode 100644 index 0000000000..47bde22500 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/disk_import_service.go @@ -0,0 +1,802 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "context" + "fmt" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/common/pvc" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements/copier" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ( + cdiDataVolName = "cdi-data-vol" + cdiScratchVolName = "cdi-scratch-vol" + cdiImporterDataDir = "/data" + cdiScratchDataDir = "/scratch" + cdiWriteBlockPath = "/dev/cdi-block-volume" + cdiSourceBlockPath = "/dev/source-block-volume" + sourceRegistry = "registry" + sourcePVC = "pvc" + cloneStrategySnapshot = "snapshot" + cloneStrategyCSI = "csi-clone" + cloneStrategyHost = "host-assisted" +) + +type PVCImportSource struct { + Registry *PVCImportSourceRegistry + PVC *PVCImportSourcePVC +} + +type PVCImportSourceRegistry struct { + URL string + Secret string + CertConfigMap string +} + +type PVCImportSourcePVC struct { + Name string + Namespace string +} + +func NewPVCRegistryImportSource(url, secret, certConfigMap string) *PVCImportSource { + return &PVCImportSource{ + Registry: &PVCImportSourceRegistry{ + URL: url, + Secret: secret, + CertConfigMap: certConfigMap, + }, + } +} + +func NewPVCPVCImportSource(name, namespace string) *PVCImportSource { + return &PVCImportSource{ + PVC: &PVCImportSourcePVC{ + Name: name, + Namespace: namespace, + }, + } +} + +func (s DiskService) StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + if sc == nil { + return fmt.Errorf("cannot create import PVC: StorageClass must not be nil") + } + if source != nil && source.PVC != nil { + return s.startPVCClone(ctx, pvcSize, sc, source.PVC, vd, nodePlacement) + } + + volumeMode, accessMode, err := s.GetVolumeAndAccessModes(ctx, vd, sc) + if err != nil { + return fmt.Errorf("get volume and access modes: %w", err) + } + + target := &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{Kind: "PersistentVolumeClaim", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: vd.Status.Target.PersistentVolumeClaim, + Namespace: vd.Namespace, + Annotations: s.pvcImportAnnotations(source, pvcSize), + Finalizers: s.diskProtectionFinalizers(), + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualDiskKind, + Name: vd.Name, + UID: vd.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }}, + }, + Spec: *pvc.CreateSpec(&sc.Name, pvcSize, accessMode, volumeMode), + } + + if nodePlacement != nil { + if err := provisioner.KeepNodePlacementTolerations(nodePlacement, target); err != nil { + return fmt.Errorf("keep node placement: %w", err) + } + } + + err = s.client.Create(ctx, target) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (s DiskService) StartSupplementPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *PVCImportSource, owner client.Object, sup supplements.Generator, nodePlacement *provisioner.NodePlacement) error { + if sc == nil { + return fmt.Errorf("cannot create import PVC: StorageClass must not be nil") + } + + volumeMode, accessMode, err := s.GetVolumeAndAccessModes(ctx, owner, sc) + if err != nil { + return fmt.Errorf("get volume and access modes: %w", err) + } + + target := &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{Kind: "PersistentVolumeClaim", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: sup.PersistentVolumeClaim().Name, + Namespace: sup.PersistentVolumeClaim().Namespace, + Annotations: s.pvcImportAnnotations(source, pvcSize), + Finalizers: s.diskProtectionFinalizers(), + OwnerReferences: []metav1.OwnerReference{ownerReferenceForObject(owner)}, + }, + Spec: *pvc.CreateSpec(&sc.Name, pvcSize, accessMode, volumeMode), + } + + if nodePlacement != nil { + if err := provisioner.KeepNodePlacementTolerations(nodePlacement, target); err != nil { + return fmt.Errorf("keep node placement: %w", err) + } + } + + err = s.client.Create(ctx, target) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (s DiskService) EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + sup := supplements.NewGenerator(annotations.VDShortName, vd.Name, vd.Namespace, vd.UID) + return s.EnsureSupplementPVCImport(ctx, target, source, vd, sup, nodePlacement) +} + +func (s DiskService) EnsureSupplementPVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *PVCImportSource, owner client.Object, sup supplements.Generator, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + if source != nil && source.PVC != nil && isSmartCloneStrategy(target.Annotations[annotations.AnnPVCImportCloneStrategy]) { + if target.Status.Phase == corev1.ClaimBound { + if err := s.patchTargetImportPhase(ctx, target, corev1.PodSucceeded); err != nil { + return "", err + } + return corev1.PodSucceeded, s.cleanupPVCImportClone(ctx, target) + } + return corev1.PodPending, nil + } + + phase := corev1.PodPhase(target.Annotations[annotations.AnnPVCImportPhase]) + if phase == corev1.PodSucceeded { + _, err := s.cleanupPVCImport(ctx, sup, target) + return phase, err + } + + if err := s.ensurePVCImportSupplements(ctx, target, sup); err != nil { + return "", err + } + + scratch, err := s.ensurePVCImportScratch(ctx, target) + if err != nil { + return "", err + } + + var sourceClaim *corev1.PersistentVolumeClaim + if source != nil && source.PVC != nil { + sourceClaim, err = object.FetchObject(ctx, types.NamespacedName{Name: source.PVC.Name, Namespace: source.PVC.Namespace}, s.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + return "", fmt.Errorf("fetch source pvc: %w", err) + } + if sourceClaim == nil { + return "", fmt.Errorf("source pvc %s/%s not found", source.PVC.Namespace, source.PVC.Name) + } + } + + podKey := sup.PVCImporterPod() + target.Annotations[annotations.AnnPVCImportPod] = podKey.Name + pod, err := object.FetchObject(ctx, podKey, s.client, &corev1.Pod{}) + if err != nil { + return "", fmt.Errorf("fetch importer pod: %w", err) + } + if pod == nil { + pod = s.makePVCImporterPod(podKey.Name, target, source, sourceClaim, scratch.Name, nodePlacement) + if err := s.client.Create(ctx, pod); err != nil && !k8serrors.IsAlreadyExists(err) { + return "", fmt.Errorf("create importer pod: %w", err) + } + return corev1.PodPending, s.patchTargetImportPhase(ctx, target, corev1.PodPending) + } + + if pod.Status.Phase != "" && pod.Status.Phase != phase { + if err := s.patchTargetImportPhase(ctx, target, pod.Status.Phase); err != nil { + return "", err + } + } + if pod.Status.Phase == corev1.PodSucceeded { + _, err := s.cleanupPVCImport(ctx, sup, target) + return pod.Status.Phase, err + } + return pod.Status.Phase, nil +} + +// pvcImportAnnotations builds the annotations applied on a target PVC to +// describe how its contents should be imported. The source can be a DVCR +// registry image (used by Upload/HTTP/Registry and ObjectRef CVI/VI data +// sources) or another PVC (used when cloning from a VirtualDisk). +// diskProtectionFinalizers returns the finalizer slice applied to PVCs that +// belong to a VirtualDisk/VirtualImage. The finalizer ensures the controller +// has a chance to perform explicit cleanup before garbage collection deletes +// the PVC. It is applied at creation time so no separate Protect call is +// required afterwards. +func (s DiskService) diskProtectionFinalizers() []string { + if s.protection == nil { + return nil + } + finalizer := s.protection.GetFinalizer() + if finalizer == "" { + return nil + } + return []string{finalizer} +} + +func (s DiskService) pvcImportAnnotations(source *PVCImportSource, size resource.Quantity) map[string]string { + anno := map[string]string{ + annotations.AnnPVCImportPod: "", + annotations.AnnPVCImportPhase: string(corev1.PodPending), + annotations.AnnPVCImportImageSize: size.String(), + } + if source != nil && source.Registry != nil { + anno[annotations.AnnPVCImportSource] = sourceRegistry + if source.Registry.URL != "" { + anno[annotations.AnnPVCImportEndpoint] = source.Registry.URL + } + if source.Registry.Secret != "" { + anno[annotations.AnnPVCImportSecret] = source.Registry.Secret + } + if source.Registry.CertConfigMap != "" { + anno[annotations.AnnPVCImportCertConfigMap] = source.Registry.CertConfigMap + } + } + if source != nil && source.PVC != nil { + anno[annotations.AnnPVCImportSource] = sourcePVC + anno[annotations.AnnPVCImportEndpoint] = source.PVC.Namespace + "/" + source.PVC.Name + } + return anno +} + +func (s DiskService) ensurePVCImportSupplements(ctx context.Context, target *corev1.PersistentVolumeClaim, supGen supplements.Generator) error { + if s.dvcrSettings == nil { + return nil + } + + ownerRef := metav1.OwnerReference{ + APIVersion: "v1", + Kind: "PersistentVolumeClaim", + Name: target.Name, + UID: target.UID, + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(true), + } + + if s.dvcrSettings.AuthSecret != "" { + authCopier := copier.AuthSecret{ + Secret: copier.Secret{ + Source: types.NamespacedName{ + Name: s.dvcrSettings.AuthSecret, + Namespace: s.dvcrSettings.AuthSecretNamespace, + }, + Destination: supGen.DVCRAuthSecretForDV(), + OwnerReference: ownerRef, + }, + } + if err := authCopier.CopyCDICompatible(ctx, s.client, s.dvcrSettings.RegistryURL); err != nil { + return fmt.Errorf("copy dvcr auth secret: %w", err) + } + } + + if s.dvcrSettings.CertsSecret != "" { + caBundleCopier := copier.CABundleConfigMap{ + SourceSecret: types.NamespacedName{ + Name: s.dvcrSettings.CertsSecret, + Namespace: s.dvcrSettings.CertsSecretNamespace, + }, + Destination: supGen.DVCRCABundleConfigMapForDV(), + OwnerReference: ownerRef, + } + if err := caBundleCopier.Copy(ctx, s.client); err != nil { + return fmt.Errorf("copy dvcr ca bundle: %w", err) + } + } + + return nil +} + +func (s DiskService) ensurePVCImportScratch(ctx context.Context, target *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) { + name := target.Name + "-scratch" + scratch, err := object.FetchObject(ctx, types.NamespacedName{Name: name, Namespace: target.Namespace}, s.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + return nil, fmt.Errorf("fetch scratch pvc: %w", err) + } + if scratch != nil { + return scratch, nil + } + + size := scratchPVCSize(target.Spec.Resources.Requests[corev1.ResourceStorage]) + volumeMode := corev1.PersistentVolumeFilesystem + scratch = &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{Kind: "PersistentVolumeClaim", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: target.Namespace, + Labels: map[string]string{ + annotations.QuotaExcludeLabel: annotations.QuotaExcludeValue, + }, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "PersistentVolumeClaim", + Name: target.Name, + UID: target.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }}, + }, + Spec: target.Spec, + } + scratch.Spec.VolumeName = "" + scratch.Spec.VolumeMode = &volumeMode + scratch.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} + scratch.Spec.Resources.Requests[corev1.ResourceStorage] = size + if err := s.client.Create(ctx, scratch); err != nil && !k8serrors.IsAlreadyExists(err) { + return nil, fmt.Errorf("create scratch pvc: %w", err) + } + return scratch, nil +} + +func scratchPVCSize(targetSize resource.Quantity) resource.Quantity { + size := targetSize.DeepCopy() + minOverhead := resource.MustParse("256Mi") + overhead := *resource.NewQuantity(targetSize.Value()/10, targetSize.Format) + if overhead.Cmp(minOverhead) < 0 { + overhead = minOverhead + } + size.Add(overhead) + return size +} + +func (s DiskService) makePVCImporterPod(podName string, target *corev1.PersistentVolumeClaim, source *PVCImportSource, sourceClaim *corev1.PersistentVolumeClaim, scratchName string, nodePlacement *provisioner.NodePlacement) *corev1.Pod { + podAnnotations := map[string]string{annotations.AnnPVCImportCreatedBy: "yes"} + target.Annotations[annotations.AnnPVCImportPod] = podName + + container := corev1.Container{ + Name: "d8v-cdi-importer", + Image: s.diskImporterImage, + ImagePullPolicy: corev1.PullPolicy(s.pullPolicy), + Command: []string{"/usr/bin/cdi-importer"}, + Args: []string{"-v=" + s.verbose}, + Env: []corev1.EnvVar{ + {Name: common.ImporterSource, Value: sourceRegistry}, + {Name: common.ImporterEndpoint, Value: target.Annotations[annotations.AnnPVCImportEndpoint]}, + {Name: common.ImporterContentType, Value: "kubevirt"}, + {Name: common.ImporterImageSize, Value: target.Annotations[annotations.AnnPVCImportImageSize]}, + {Name: common.OwnerUID, Value: string(target.UID)}, + {Name: common.FilesystemOverheadVar, Value: "0"}, + {Name: common.InsecureTLSVar, Value: "false"}, + {Name: "PREALLOCATION", Value: "false"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: cdiScratchVolName, MountPath: cdiScratchDataDir}, {Name: "tmp", MountPath: "/tmp"}}, + Ports: []corev1.ContainerPort{{Name: "metrics", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}}, + } + if s.resourceRequirements.Requests != nil || s.resourceRequirements.Limits != nil { + container.Resources = s.resourceRequirements + } + if secretName := target.Annotations[annotations.AnnPVCImportSecret]; secretName != "" { + container.Env = append(container.Env, corev1.EnvVar{ + Name: common.ImporterAccessKeyID, + ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: importer.KeyAccess, + }}, + }, corev1.EnvVar{ + Name: common.ImporterSecretKey, + ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: importer.KeySecret, + }}, + }) + } + if target.Annotations[annotations.AnnPVCImportCertConfigMap] != "" { + container.Env = append(container.Env, corev1.EnvVar{Name: common.ImporterCertDirVar, Value: common.ImporterCertDir}) + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{Name: "cert-vol", MountPath: common.ImporterCertDir}) + } + if target.Spec.VolumeMode != nil && *target.Spec.VolumeMode == corev1.PersistentVolumeBlock { + container.VolumeDevices = []corev1.VolumeDevice{{Name: cdiDataVolName, DevicePath: cdiWriteBlockPath}} + } else { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{Name: cdiDataVolName, MountPath: cdiImporterDataDir}) + } + + volumes := []corev1.Volume{ + {Name: "tmp", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + {Name: cdiDataVolName, VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: target.Name}}}, + {Name: cdiScratchVolName, VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: scratchName}}}, + } + if certConfigMap := target.Annotations[annotations.AnnPVCImportCertConfigMap]; certConfigMap != "" { + volumes = append(volumes, corev1.Volume{ + Name: "cert-vol", + VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: certConfigMap}, + }}, + }) + } + if source != nil && source.PVC != nil && sourceClaim != nil { + sourcePath := "/source/disk.img" + if sourceClaim.Spec.VolumeMode != nil && *sourceClaim.Spec.VolumeMode == corev1.PersistentVolumeBlock { + sourcePath = cdiSourceBlockPath + container.VolumeDevices = append(container.VolumeDevices, corev1.VolumeDevice{Name: "source-vol", DevicePath: cdiSourceBlockPath}) + } else { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{Name: "source-vol", MountPath: "/source", ReadOnly: true}) + } + + targetPath := cdiImporterDataDir + "/disk.img" + if target.Spec.VolumeMode != nil && *target.Spec.VolumeMode == corev1.PersistentVolumeBlock { + targetPath = cdiWriteBlockPath + } + + container.Command = []string{"/usr/bin/qemu-img"} + container.Args = []string{"convert", "-p", "-O", "raw", sourcePath, targetPath} + container.Env = nil + container.Ports = nil + volumes = append(volumes, corev1.Volume{ + Name: "source-vol", + VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: source.PVC.Name, + ReadOnly: true, + }}, + }) + } + + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: target.Namespace, + Annotations: podAnnotations, + Labels: map[string]string{ + annotations.QuotaExcludeLabel: annotations.QuotaExcludeValue, + }, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "PersistentVolumeClaim", + Name: target.Name, + UID: target.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{container}, + RestartPolicy: corev1.RestartPolicyOnFailure, + Volumes: volumes, + }, + } + podutil.SetRestrictedSecurityContext(&pod.Spec) + if nodePlacement != nil { + pod.Spec.Tolerations = nodePlacement.Tolerations + _ = provisioner.KeepNodePlacementTolerations(nodePlacement, pod) + } + return pod +} + +func (s DiskService) patchTargetImportPhase(ctx context.Context, target *corev1.PersistentVolumeClaim, phase corev1.PodPhase) error { + copy := target.DeepCopy() + if copy.Annotations == nil { + copy.Annotations = map[string]string{} + } + copy.Annotations[annotations.AnnPVCImportPhase] = string(phase) + return s.client.Patch(ctx, copy, client.MergeFrom(target)) +} + +func (s DiskService) cleanupPVCImport(ctx context.Context, sup supplements.Generator, target *corev1.PersistentVolumeClaim) (bool, error) { + var deleted bool + podName := target.Annotations[annotations.AnnPVCImportPod] + if podName == "" { + podName = sup.PVCImporterPod().Name + } + for _, obj := range []client.Object{ + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: target.Namespace}}, + &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: target.Name + "-scratch", Namespace: target.Namespace}}, + } { + err := s.client.Delete(ctx, obj) + switch { + case err == nil: + deleted = true + case !k8serrors.IsNotFound(err): + return false, err + } + } + return deleted, nil +} + +func ownerReferenceForObject(obj client.Object) metav1.OwnerReference { + gvk := obj.GetObjectKind().GroupVersionKind() + return metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: obj.GetName(), + UID: obj.GetUID(), + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + } +} + +func (s DiskService) startPVCClone(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *PVCImportSourcePVC, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + sourceClaim, err := object.FetchObject(ctx, types.NamespacedName{Name: source.Name, Namespace: source.Namespace}, s.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + return fmt.Errorf("fetch source pvc: %w", err) + } + if sourceClaim == nil { + return fmt.Errorf("source pvc %s/%s not found", source.Namespace, source.Name) + } + + volumeMode, accessMode, err := s.GetVolumeAndAccessModes(ctx, vd, sc) + if err != nil { + return fmt.Errorf("get volume and access modes: %w", err) + } + + strategy := s.choosePVCCloneStrategy(ctx, sourceClaim, sc, volumeMode) + target := s.makePVCCloneTarget(pvcSize, sc, accessMode, volumeMode, sourceClaim, vd, strategy) + if nodePlacement != nil { + if err := provisioner.KeepNodePlacementTolerations(nodePlacement, target); err != nil { + return fmt.Errorf("keep node placement: %w", err) + } + } + + if strategy == cloneStrategySnapshot { + if err := s.ensureCloneSnapshot(ctx, sourceClaim, target, vd); err != nil { + return err + } + } + + err = s.client.Create(ctx, target) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + return nil +} + +func (s DiskService) choosePVCCloneStrategy(ctx context.Context, sourceClaim *corev1.PersistentVolumeClaim, targetSC *storagev1.StorageClass, targetVolumeMode corev1.PersistentVolumeMode) string { + sourceSC, err := s.getPVCStorageClass(ctx, sourceClaim) + if err != nil || sourceSC == nil { + return cloneStrategyHost + } + + preferred := cloneStrategySnapshot + if sp, err := object.FetchObject(ctx, types.NamespacedName{Name: targetSC.Name}, s.client, &cdiv1.StorageProfile{}); err == nil && sp != nil && sp.Status.CloneStrategy != nil { + switch *sp.Status.CloneStrategy { + case cdiv1.CloneStrategyCsiClone: + preferred = cloneStrategyCSI + case cdiv1.CloneStrategyHostAssisted: + preferred = cloneStrategyHost + case cdiv1.CloneStrategySnapshot: + preferred = cloneStrategySnapshot + } + } + + if preferred == cloneStrategySnapshot && s.canSnapshotClone(ctx, sourceClaim, sourceSC, targetSC, targetVolumeMode) { + return cloneStrategySnapshot + } + if preferred != cloneStrategyHost && s.canCSIClone(sourceClaim, sourceSC, targetSC, targetVolumeMode) { + return cloneStrategyCSI + } + if preferred == cloneStrategyCSI && s.canSnapshotClone(ctx, sourceClaim, sourceSC, targetSC, targetVolumeMode) { + return cloneStrategySnapshot + } + return cloneStrategyHost +} + +func (s DiskService) makePVCCloneTarget(pvcSize resource.Quantity, sc *storagev1.StorageClass, accessMode corev1.PersistentVolumeAccessMode, volumeMode corev1.PersistentVolumeMode, sourceClaim *corev1.PersistentVolumeClaim, vd *v1alpha2.VirtualDisk, strategy string) *corev1.PersistentVolumeClaim { + pvcSize = pvcCloneTargetSize(pvcSize, sourceClaim) + pvcAnnotations := map[string]string{ + annotations.AnnPVCImportSource: sourcePVC, + annotations.AnnPVCImportEndpoint: sourceClaim.Namespace + "/" + sourceClaim.Name, + annotations.AnnPVCImportCloneStrategy: strategy, + annotations.AnnPVCImportImageSize: pvcSize.String(), + annotations.AnnPVCImportPhase: string(corev1.PodPending), + annotations.AnnPVCImportPod: "", + } + + target := &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{Kind: "PersistentVolumeClaim", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: vd.Status.Target.PersistentVolumeClaim, + Namespace: vd.Namespace, + Annotations: pvcAnnotations, + Finalizers: s.diskProtectionFinalizers(), + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualDiskKind, + Name: vd.Name, + UID: vd.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }}, + }, + Spec: *pvc.CreateSpec(&sc.Name, pvcSize, accessMode, volumeMode), + } + + switch strategy { + case cloneStrategySnapshot: + snapshotName := target.Name + "-clone-snapshot" + target.Annotations[annotations.AnnPVCImportCloneSnapshot] = snapshotName + target.Spec.DataSource = &corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("snapshot.storage.k8s.io"), + Kind: "VolumeSnapshot", + Name: snapshotName, + } + target.Spec.DataSourceRef = &corev1.TypedObjectReference{ + APIGroup: ptr.To("snapshot.storage.k8s.io"), + Kind: "VolumeSnapshot", + Name: snapshotName, + } + case cloneStrategyCSI: + target.Spec.DataSource = &corev1.TypedLocalObjectReference{ + Kind: "PersistentVolumeClaim", + Name: sourceClaim.Name, + } + target.Spec.DataSourceRef = &corev1.TypedObjectReference{ + Kind: "PersistentVolumeClaim", + Name: sourceClaim.Name, + } + } + return target +} + +func pvcCloneTargetSize(requested resource.Quantity, sourceClaim *corev1.PersistentVolumeClaim) resource.Quantity { + size := requested.DeepCopy() + for _, candidate := range []resource.Quantity{ + sourceClaim.Spec.Resources.Requests[corev1.ResourceStorage], + sourceClaim.Status.Capacity[corev1.ResourceStorage], + } { + if !candidate.IsZero() && size.Cmp(candidate) < 0 { + size = candidate.DeepCopy() + } + } + return size +} + +func (s DiskService) ensureCloneSnapshot(ctx context.Context, sourceClaim, target *corev1.PersistentVolumeClaim, vd *v1alpha2.VirtualDisk) error { + snapshotName := target.Annotations[annotations.AnnPVCImportCloneSnapshot] + if snapshotName == "" { + return fmt.Errorf("clone snapshot annotation is empty") + } + existing, err := object.FetchObject(ctx, types.NamespacedName{Name: snapshotName, Namespace: target.Namespace}, s.client, &vsv1.VolumeSnapshot{}) + if err != nil { + return fmt.Errorf("fetch clone snapshot: %w", err) + } + if existing != nil { + return nil + } + + sourceSC, err := s.getPVCStorageClass(ctx, sourceClaim) + if err != nil { + return err + } + snapshotClass := s.snapshotClassForProvisioner(ctx, sourceSC.Provisioner) + if snapshotClass == "" { + return fmt.Errorf("no compatible VolumeSnapshotClass found for provisioner %q", sourceSC.Provisioner) + } + + vs := &vsv1.VolumeSnapshot{ + TypeMeta: metav1.TypeMeta{Kind: "VolumeSnapshot", APIVersion: "snapshot.storage.k8s.io/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: snapshotName, + Namespace: target.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualDiskKind, + Name: vd.Name, + UID: vd.UID, + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(true), + }}, + }, + Spec: vsv1.VolumeSnapshotSpec{ + Source: vsv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: ptr.To(sourceClaim.Name), + }, + VolumeSnapshotClassName: ptr.To(snapshotClass), + }, + } + if err := s.client.Create(ctx, vs); err != nil && !k8serrors.IsAlreadyExists(err) { + return fmt.Errorf("create clone snapshot: %w", err) + } + return nil +} + +func (s DiskService) canSnapshotClone(ctx context.Context, sourceClaim *corev1.PersistentVolumeClaim, sourceSC, targetSC *storagev1.StorageClass, targetVolumeMode corev1.PersistentVolumeMode) bool { + return sourceSC.Provisioner == targetSC.Provisioner && + volumeModesEqual(sourceClaim, targetVolumeMode) && + s.snapshotClassForProvisioner(ctx, sourceSC.Provisioner) != "" +} + +func (s DiskService) canCSIClone(sourceClaim *corev1.PersistentVolumeClaim, sourceSC, targetSC *storagev1.StorageClass, targetVolumeMode corev1.PersistentVolumeMode) bool { + return sourceClaim.Namespace != "" && + sourceSC.Provisioner == targetSC.Provisioner && + volumeModesEqual(sourceClaim, targetVolumeMode) +} + +func (s DiskService) getPVCStorageClass(ctx context.Context, claim *corev1.PersistentVolumeClaim) (*storagev1.StorageClass, error) { + if claim.Spec.StorageClassName == nil || *claim.Spec.StorageClassName == "" { + return nil, fmt.Errorf("source pvc %s/%s has no storageClassName", claim.Namespace, claim.Name) + } + sc, err := object.FetchObject(ctx, types.NamespacedName{Name: *claim.Spec.StorageClassName}, s.client, &storagev1.StorageClass{}) + if err != nil { + return nil, fmt.Errorf("fetch source storage class: %w", err) + } + if sc == nil { + return nil, fmt.Errorf("source storage class %q not found", *claim.Spec.StorageClassName) + } + return sc, nil +} + +func (s DiskService) snapshotClassForProvisioner(ctx context.Context, provisioner string) string { + var list vsv1.VolumeSnapshotClassList + if err := s.client.List(ctx, &list); err != nil { + return "" + } + for _, item := range list.Items { + if item.Driver == provisioner { + return item.Name + } + } + return "" +} + +func volumeModesEqual(sourceClaim *corev1.PersistentVolumeClaim, targetVolumeMode corev1.PersistentVolumeMode) bool { + sourceMode := corev1.PersistentVolumeFilesystem + if sourceClaim.Spec.VolumeMode != nil { + sourceMode = *sourceClaim.Spec.VolumeMode + } + return sourceMode == targetVolumeMode +} + +func isSmartCloneStrategy(strategy string) bool { + return strategy == cloneStrategySnapshot || strategy == cloneStrategyCSI +} + +func (s DiskService) cleanupPVCImportClone(ctx context.Context, target *corev1.PersistentVolumeClaim) error { + if target.Annotations[annotations.AnnPVCImportCloneStrategy] != cloneStrategySnapshot { + return nil + } + snapshotName := target.Annotations[annotations.AnnPVCImportCloneSnapshot] + if snapshotName == "" { + return nil + } + err := s.client.Delete(ctx, &vsv1.VolumeSnapshot{ObjectMeta: metav1.ObjectMeta{Name: snapshotName, Namespace: target.Namespace}}) + if err != nil && !k8serrors.IsNotFound(err) { + return err + } + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/service/disk_import_service_test.go b/images/virtualization-artifact/pkg/controller/service/disk_import_service_test.go new file mode 100644 index 0000000000..3b90e4398b --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/disk_import_service_test.go @@ -0,0 +1,390 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "context" + "testing" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func TestDiskServiceStartPVCImportCreatesTargetPVC(t *testing.T) { + ctx := context.Background() + c := fake.NewClientBuilder().WithScheme(diskImportTestScheme(t)).Build() + svc := NewDiskService(c, nil, nil, "test", DiskImporterConfig{Image: "disk-importer:latest", PullPolicy: string(corev1.PullIfNotPresent), Verbose: "3"}) + + url := "docker://registry.example/image:tag" + secret := "auth" + cert := "ca" + vd := diskImportTestVD() + sc := &storagev1.StorageClass{ObjectMeta: metav1.ObjectMeta{ + Name: "fast", + Annotations: map[string]string{ + annotations.AnnVirtualDiskVolumeMode: string(corev1.PersistentVolumeFilesystem), + annotations.AnnVirtualDiskAccessMode: string(corev1.ReadWriteOnce), + }, + }} + + err := svc.StartPVCImport(ctx, resource.MustParse("1Gi"), sc, NewPVCRegistryImportSource(url, secret, cert), vd, nil) + if err != nil { + t.Fatalf("StartPVCImport failed: %v", err) + } + + pvc := &corev1.PersistentVolumeClaim{} + if err := c.Get(ctx, types.NamespacedName{Name: vd.Status.Target.PersistentVolumeClaim, Namespace: vd.Namespace}, pvc); err != nil { + t.Fatalf("target pvc not found: %v", err) + } + if got := pvc.Annotations[annotations.AnnPVCImportSource]; got != sourceRegistry { + t.Fatalf("unexpected source annotation: %q", got) + } + if got := pvc.Annotations[annotations.AnnPVCImportEndpoint]; got != url { + t.Fatalf("unexpected endpoint annotation: %q", got) + } + if got := pvc.Annotations[annotations.AnnPVCImportPhase]; got != string(corev1.PodPending) { + t.Fatalf("unexpected import phase annotation: %q", got) + } + if len(pvc.OwnerReferences) != 1 || pvc.OwnerReferences[0].Kind != v1alpha2.VirtualDiskKind { + t.Fatalf("target pvc owner reference not set: %#v", pvc.OwnerReferences) + } +} + +func TestDiskServiceEnsurePVCImportIsResumable(t *testing.T) { + ctx := context.Background() + vd := diskImportTestVD() + target := diskImportTargetPVC(vd) + importerPodName := diskImportImporterPodName(vd) + c := fake.NewClientBuilder().WithScheme(diskImportTestScheme(t)).WithObjects(target).Build() + svc := NewDiskService(c, nil, nil, "test", DiskImporterConfig{Image: "disk-importer:latest", PullPolicy: string(corev1.PullIfNotPresent), Verbose: "3"}) + + phase, err := svc.EnsurePVCImport(ctx, target, NewPVCRegistryImportSource("", "", ""), vd, nil) + if err != nil { + t.Fatalf("EnsurePVCImport failed: %v", err) + } + if phase != corev1.PodPending { + t.Fatalf("unexpected phase after first ensure: %s", phase) + } + + for _, key := range []types.NamespacedName{ + {Name: target.Name + "-scratch", Namespace: target.Namespace}, + {Name: importerPodName, Namespace: target.Namespace}, + } { + obj := &corev1.PersistentVolumeClaim{} + if key.Name == importerPodName { + pod := &corev1.Pod{} + if err := c.Get(ctx, key, pod); err != nil { + t.Fatalf("pod %s not found: %v", key.Name, err) + } + continue + } + if err := c.Get(ctx, key, obj); err != nil { + t.Fatalf("scratch pvc %s not found: %v", key.Name, err) + } + } + + pod := &corev1.Pod{} + if err := c.Get(ctx, types.NamespacedName{Name: importerPodName, Namespace: target.Namespace}, pod); err != nil { + t.Fatalf("get pod: %v", err) + } + + scratch := &corev1.PersistentVolumeClaim{} + if err := c.Get(ctx, types.NamespacedName{Name: target.Name + "-scratch", Namespace: target.Namespace}, scratch); err != nil { + t.Fatalf("get scratch pvc: %v", err) + } + if got, want := scratch.Spec.Resources.Requests[corev1.ResourceStorage], resource.MustParse("1342177280"); got.Cmp(want) != 0 { + t.Fatalf("unexpected scratch size: got %s, want %s", got.String(), want.String()) + } + + pod.Status.Phase = corev1.PodSucceeded + if err := c.Status().Update(ctx, pod); err != nil { + t.Fatalf("update pod status: %v", err) + } + if err := c.Get(ctx, client.ObjectKeyFromObject(target), target); err != nil { + t.Fatalf("refresh target: %v", err) + } + + phase, err = svc.EnsurePVCImport(ctx, target, NewPVCRegistryImportSource("", "", ""), vd, nil) + if err != nil { + t.Fatalf("EnsurePVCImport after pod success failed: %v", err) + } + if phase != corev1.PodSucceeded { + t.Fatalf("unexpected final phase: %s", phase) + } + if err := c.Get(ctx, types.NamespacedName{Name: target.Name + "-scratch", Namespace: target.Namespace}, &corev1.PersistentVolumeClaim{}); client.IgnoreNotFound(err) == nil && err == nil { + t.Fatalf("scratch pvc still exists") + } + if err := c.Get(ctx, types.NamespacedName{Name: importerPodName, Namespace: target.Namespace}, &corev1.Pod{}); client.IgnoreNotFound(err) == nil && err == nil { + t.Fatalf("import pod still exists") + } +} + +func TestDiskServiceStartPVCCloneUsesVolumeSnapshotWhenPossible(t *testing.T) { + ctx := context.Background() + vd := diskImportTestVD() + sc := diskImportStorageClass() + sourceClaim := diskImportSourcePVC() + snapshotClass := &vsv1.VolumeSnapshotClass{ + ObjectMeta: metav1.ObjectMeta{Name: "snap-fast"}, + Driver: sc.Provisioner, + } + c := fake.NewClientBuilder().WithScheme(diskImportTestScheme(t)).WithObjects(sc, sourceClaim, snapshotClass).Build() + svc := NewDiskService(c, nil, nil, "test") + + err := svc.StartPVCImport(ctx, resource.MustParse("1Gi"), sc, NewPVCPVCImportSource(sourceClaim.Name, sourceClaim.Namespace), vd, nil) + if err != nil { + t.Fatalf("StartPVCImport failed: %v", err) + } + + target := &corev1.PersistentVolumeClaim{} + if err := c.Get(ctx, types.NamespacedName{Name: vd.Status.Target.PersistentVolumeClaim, Namespace: vd.Namespace}, target); err != nil { + t.Fatalf("target pvc not found: %v", err) + } + if got := target.Annotations[annotations.AnnPVCImportCloneStrategy]; got != cloneStrategySnapshot { + t.Fatalf("unexpected clone strategy: %q", got) + } + if got := target.Annotations[annotations.AnnPVCImportPhase]; got != string(corev1.PodPending) { + t.Fatalf("unexpected import phase: %q", got) + } + if target.Spec.DataSourceRef == nil || target.Spec.DataSourceRef.Kind != "VolumeSnapshot" { + t.Fatalf("target pvc does not reference VolumeSnapshot: %#v", target.Spec.DataSourceRef) + } + if got, want := target.Spec.Resources.Requests[corev1.ResourceStorage], resource.MustParse("2Gi"); got.Cmp(want) != 0 { + t.Fatalf("unexpected target size: got %s, want %s", got.String(), want.String()) + } + + snapshot := &vsv1.VolumeSnapshot{} + if err := c.Get(ctx, types.NamespacedName{Name: target.Annotations[annotations.AnnPVCImportCloneSnapshot], Namespace: vd.Namespace}, snapshot); err != nil { + t.Fatalf("clone snapshot not found: %v", err) + } + if snapshot.Spec.Source.PersistentVolumeClaimName == nil || *snapshot.Spec.Source.PersistentVolumeClaimName != sourceClaim.Name { + t.Fatalf("unexpected snapshot source: %#v", snapshot.Spec.Source.PersistentVolumeClaimName) + } +} + +func TestDiskServiceEnsurePVCCloneMarksSucceededAndCleansSnapshot(t *testing.T) { + ctx := context.Background() + vd := diskImportTestVD() + target := diskImportTargetPVC(vd) + target.Annotations[annotations.AnnPVCImportSource] = sourcePVC + target.Annotations[annotations.AnnPVCImportCloneStrategy] = cloneStrategySnapshot + target.Annotations[annotations.AnnPVCImportCloneSnapshot] = target.Name + "-clone-snapshot" + target.Annotations[annotations.AnnPVCImportPhase] = string(corev1.PodPending) + snapshot := &vsv1.VolumeSnapshot{ObjectMeta: metav1.ObjectMeta{Name: target.Annotations[annotations.AnnPVCImportCloneSnapshot], Namespace: target.Namespace}} + c := fake.NewClientBuilder().WithScheme(diskImportTestScheme(t)).WithObjects(target, snapshot).Build() + svc := NewDiskService(c, nil, nil, "test") + + phase, err := svc.EnsurePVCImport(ctx, target, NewPVCPVCImportSource("source", "default"), vd, nil) + if err != nil { + t.Fatalf("EnsurePVCImport failed: %v", err) + } + if phase != corev1.PodSucceeded { + t.Fatalf("unexpected phase: %s", phase) + } + if err := c.Get(ctx, types.NamespacedName{Name: snapshot.Name, Namespace: snapshot.Namespace}, &vsv1.VolumeSnapshot{}); client.IgnoreNotFound(err) == nil && err == nil { + t.Fatalf("clone snapshot still exists") + } + refreshed := &corev1.PersistentVolumeClaim{} + if err := c.Get(ctx, client.ObjectKeyFromObject(target), refreshed); err != nil { + t.Fatalf("refresh target: %v", err) + } + if got := refreshed.Annotations[annotations.AnnPVCImportPhase]; got != string(corev1.PodSucceeded) { + t.Fatalf("unexpected import phase: %q", got) + } +} + +func TestDiskServiceEnsurePVCCloneHostAssistedUsesShelllessQemuImgCommand(t *testing.T) { + ctx := context.Background() + vd := diskImportTestVD() + target := diskImportTargetPVC(vd) + importerPodName := diskImportImporterPodName(vd) + target.Annotations[annotations.AnnPVCImportSource] = sourcePVC + target.Annotations[annotations.AnnPVCImportCloneStrategy] = cloneStrategyHost + target.Annotations[annotations.AnnPVCImportEndpoint] = "default/source" + target.Annotations[annotations.AnnPVCImportPhase] = string(corev1.PodPending) + target.Spec.VolumeMode = ptr.To(corev1.PersistentVolumeBlock) + sourceClaim := diskImportSourcePVC() + sourceClaim.Spec.VolumeMode = ptr.To(corev1.PersistentVolumeBlock) + c := fake.NewClientBuilder().WithScheme(diskImportTestScheme(t)).WithObjects(target, sourceClaim).Build() + svc := NewDiskService(c, nil, nil, "test", DiskImporterConfig{Image: "disk-importer:latest", PullPolicy: string(corev1.PullIfNotPresent), Verbose: "3"}) + + phase, err := svc.EnsurePVCImport(ctx, target, NewPVCPVCImportSource(sourceClaim.Name, sourceClaim.Namespace), vd, nil) + if err != nil { + t.Fatalf("EnsurePVCImport failed: %v", err) + } + if phase != corev1.PodPending { + t.Fatalf("unexpected phase: %s", phase) + } + + pod := &corev1.Pod{} + if err := c.Get(ctx, types.NamespacedName{Name: importerPodName, Namespace: target.Namespace}, pod); err != nil { + t.Fatalf("get import pod: %v", err) + } + if len(pod.Spec.Containers) != 1 { + t.Fatalf("unexpected containers: %#v", pod.Spec.Containers) + } + container := pod.Spec.Containers[0] + if got := container.Command; len(got) != 1 || got[0] != "/usr/bin/qemu-img" { + t.Fatalf("unexpected command: %#v", got) + } + wantArgs := []string{"convert", "-p", "-O", "raw", cdiSourceBlockPath, cdiWriteBlockPath} + if len(container.Args) != len(wantArgs) { + t.Fatalf("unexpected args: %#v", container.Args) + } + for i := range wantArgs { + if container.Args[i] != wantArgs[i] { + t.Fatalf("unexpected args: got %#v, want %#v", container.Args, wantArgs) + } + } +} + +func TestDiskServiceStartPVCCloneFallsBackToCSIClone(t *testing.T) { + ctx := context.Background() + vd := diskImportTestVD() + sc := diskImportStorageClass() + sourceClaim := diskImportSourcePVC() + c := fake.NewClientBuilder().WithScheme(diskImportTestScheme(t)).WithObjects(sc, sourceClaim).Build() + svc := NewDiskService(c, nil, nil, "test") + + err := svc.StartPVCImport(ctx, resource.MustParse("1Gi"), sc, NewPVCPVCImportSource(sourceClaim.Name, sourceClaim.Namespace), vd, nil) + if err != nil { + t.Fatalf("StartPVCImport failed: %v", err) + } + + target := &corev1.PersistentVolumeClaim{} + if err := c.Get(ctx, types.NamespacedName{Name: vd.Status.Target.PersistentVolumeClaim, Namespace: vd.Namespace}, target); err != nil { + t.Fatalf("target pvc not found: %v", err) + } + if got := target.Annotations[annotations.AnnPVCImportCloneStrategy]; got != cloneStrategyCSI { + t.Fatalf("unexpected clone strategy: %q", got) + } + if target.Spec.DataSourceRef == nil || target.Spec.DataSourceRef.Kind != "PersistentVolumeClaim" || target.Spec.DataSourceRef.Name != sourceClaim.Name { + t.Fatalf("target pvc does not reference source PVC: %#v", target.Spec.DataSourceRef) + } +} + +func diskImportTestScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := storagev1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := vsv1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := cdiv1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := v1alpha2.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + return scheme +} + +func diskImportStorageClass() *storagev1.StorageClass { + return &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fast", + Annotations: map[string]string{ + annotations.AnnVirtualDiskVolumeMode: string(corev1.PersistentVolumeFilesystem), + annotations.AnnVirtualDiskAccessMode: string(corev1.ReadWriteOnce), + }, + }, + Provisioner: "csi.example.com", + } +} + +func diskImportSourcePVC() *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "source", Namespace: "default"}, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To("fast"), + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }}, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("2Gi"), + }, + }, + } +} + +func diskImportTestVD() *v1alpha2.VirtualDisk { + return &v1alpha2.VirtualDisk{ + TypeMeta: metav1.TypeMeta{APIVersion: v1alpha2.SchemeGroupVersion.String(), Kind: v1alpha2.VirtualDiskKind}, + ObjectMeta: metav1.ObjectMeta{ + Name: "disk", + Namespace: "default", + UID: "22222222-2222-2222-2222-222222222222", + }, + Status: v1alpha2.VirtualDiskStatus{Target: v1alpha2.DiskTarget{PersistentVolumeClaim: "d8v-vd-22222222-2222-2222-2222-222222222222-abcde"}}, + } +} + +func diskImportImporterPodName(vd *v1alpha2.VirtualDisk) string { + return "d8v-vd-pvc-importer-" + string(vd.UID) +} + +func diskImportTargetPVC(vd *v1alpha2.VirtualDisk) *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: vd.Status.Target.PersistentVolumeClaim, + Namespace: vd.Namespace, + UID: "33333333-3333-3333-3333-333333333333", + Annotations: map[string]string{ + annotations.AnnPVCImportEndpoint: "docker://registry.example/image:tag", + annotations.AnnPVCImportImageSize: "1Gi", + }, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.VirtualDiskKind, + Name: vd.Name, + UID: vd.UID, + Controller: ptr.To(true), + }}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: ptr.To("fast"), + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }}, + }, + Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimBound}, + } +} diff --git a/images/virtualization-artifact/pkg/controller/service/disk_service.go b/images/virtualization-artifact/pkg/controller/service/disk_service.go index ac5e7d0f61..c7d52d8689 100644 --- a/images/virtualization-artifact/pkg/controller/service/disk_service.go +++ b/images/virtualization-artifact/pkg/controller/service/disk_service.go @@ -20,9 +20,7 @@ import ( "context" "errors" "fmt" - "slices" "strconv" - "strings" vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" @@ -36,11 +34,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/deckhouse/virtualization-controller/pkg/common/annotations" - dvutil "github.com/deckhouse/virtualization-controller/pkg/common/datavolume" networkpolicy "github.com/deckhouse/virtualization-controller/pkg/common/network_policy" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder" "github.com/deckhouse/virtualization-controller/pkg/controller/service/volumemode" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/dvcr" @@ -48,124 +44,56 @@ import ( ) type DiskService struct { - client client.Client - dvcrSettings *dvcr.Settings - protection *ProtectionService - controllerName string + client client.Client + dvcrSettings *dvcr.Settings + protection *ProtectionService + controllerName string + diskImporterImage string + resourceRequirements corev1.ResourceRequirements + pullPolicy string + verbose string volumeAndAccessModesGetter volumemode.VolumeAndAccessModesGetter } +type DiskImporterConfig struct { + Image string + ResourceRequirements corev1.ResourceRequirements + PullPolicy string + Verbose string +} + func NewDiskService( client client.Client, dvcrSettings *dvcr.Settings, protection *ProtectionService, controllerName string, + diskImporterConfig ...DiskImporterConfig, ) *DiskService { + var cfg DiskImporterConfig + var requirements corev1.ResourceRequirements + if len(diskImporterConfig) > 0 { + cfg = diskImporterConfig[0] + requirements = cfg.ResourceRequirements + } + return &DiskService{ client: client, dvcrSettings: dvcrSettings, protection: protection, controllerName: controllerName, + diskImporterImage: cfg.Image, + resourceRequirements: requirements, + pullPolicy: cfg.PullPolicy, + verbose: cfg.Verbose, volumeAndAccessModesGetter: volumemode.NewVolumeAndAccessModesGetter(client, nil), } } -func (s DiskService) Start( - ctx context.Context, - pvcSize resource.Quantity, - sc *storagev1.StorageClass, - source *cdiv1.DataVolumeSource, - obj client.Object, - sup supplements.DataVolumeSupplement, - opts ...Option, -) error { - if sc == nil { - return errors.New("cannot create DataVolume: StorageClass must not be nil") - } - - options := newGenericOptions(opts...) - - dvBuilder := kvbuilder.NewDV(sup.DataVolume()) - dvBuilder.SetDataSource(source) - dvBuilder.SetOwnerRef(obj, obj.GetObjectKind().GroupVersionKind()) - - if options.nodePlacement != nil { - err := dvBuilder.SetNodePlacement(options.nodePlacement) - if err != nil { - return fmt.Errorf("set node placement: %w", err) - } - } - - volumeMode, accessMode, err := s.GetVolumeAndAccessModes(ctx, obj, sc) - if err != nil { - return fmt.Errorf("get volume and access modes: %w", err) - } - - dvBuilder.SetPVC(&sc.Name, pvcSize, accessMode, volumeMode) - - if s.isImmediateBindingMode(sc) { - dvBuilder.SetImmediate() - } - - dv := dvBuilder.GetResource() - err = s.client.Create(ctx, dv) - if err != nil && !k8serrors.IsAlreadyExists(err) { - return err - } - - err = networkpolicy.CreateNetworkPolicy(ctx, s.client, dv, sup, s.protection.GetFinalizer()) - if err != nil { - return fmt.Errorf("failed to create NetworkPolicy: %w", err) - } - - if source.Blank != nil || source.PVC != nil { - return nil - } - - return supplements.EnsureForDataVolume(ctx, s.client, sup, dvBuilder.GetResource(), s.dvcrSettings) -} - func (s DiskService) GetVolumeAndAccessModes(ctx context.Context, obj client.Object, sc *storagev1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) { return s.volumeAndAccessModesGetter.GetVolumeAndAccessModes(ctx, obj, sc) } -func (s DiskService) StartImmediate( - ctx context.Context, - pvcSize resource.Quantity, - sc *storagev1.StorageClass, - source *cdiv1.DataVolumeSource, - obj client.Object, - dataVolumeSupplement supplements.DataVolumeSupplement, -) error { - if sc == nil { - return errors.New("cannot create DataVolume: StorageClass must not be nil") - } - - dvBuilder := kvbuilder.NewDV(dataVolumeSupplement.DataVolume()) - dvBuilder.SetDataSource(source) - dvBuilder.SetOwnerRef(obj, obj.GetObjectKind().GroupVersionKind()) - dvBuilder.SetPVC(ptr.To(sc.GetName()), pvcSize, corev1.ReadWriteMany, corev1.PersistentVolumeBlock) - dvBuilder.SetImmediate() - dv := dvBuilder.GetResource() - - err := s.client.Create(ctx, dv) - if err != nil && !k8serrors.IsAlreadyExists(err) { - return err - } - - err = networkpolicy.CreateNetworkPolicy(ctx, s.client, dv, dataVolumeSupplement, s.protection.GetFinalizer()) - if err != nil { - return fmt.Errorf("failed to create NetworkPolicy: %w", err) - } - - if source.PVC != nil { - return nil - } - - return supplements.EnsureForDataVolume(ctx, s.client, dataVolumeSupplement, dvBuilder.GetResource(), s.dvcrSettings) -} - func (s DiskService) CheckProvisioning(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { if pvc == nil || pvc.Status.Phase == corev1.ClaimBound { return nil @@ -178,7 +106,7 @@ func (s DiskService) CheckProvisioning(ctx context.Context, pvc *corev1.Persiste pod, err := object.FetchObject(ctx, types.NamespacedName{Name: podName, Namespace: pvc.Namespace}, s.client, &corev1.Pod{}) if err != nil { - return fmt.Errorf("failed to fetch data volume provisioner %s: %w", podName, err) + return fmt.Errorf("failed to fetch pvc provisioner %s: %w", podName, err) } if pod == nil { @@ -187,7 +115,7 @@ func (s DiskService) CheckProvisioning(ctx context.Context, pvc *corev1.Persiste scheduled, _ := conditions.GetPodCondition(corev1.PodScheduled, pod.Status.Conditions) if scheduled.Status == corev1.ConditionFalse && scheduled.Reason == corev1.PodReasonUnschedulable { - return ErrDataVolumeProvisionerUnschedulable + return ErrProvisionerUnschedulable } return nil @@ -233,28 +161,18 @@ func (s DiskService) CleanUp(ctx context.Context, sup supplements.Generator) (bo } func (s DiskService) CleanUpSupplements(ctx context.Context, sup supplements.Generator) (bool, error) { - // 1. Update owner ref of pvc. - pvc, err := s.GetPersistentVolumeClaim(ctx, sup) - if err != nil { - return false, err + target := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: sup.PersistentVolumeClaim().Name, + Namespace: sup.PersistentVolumeClaim().Namespace, + }, } - - if pvc != nil { - ownerReferences := slices.DeleteFunc(pvc.OwnerReferences, func(ref metav1.OwnerReference) bool { - return ref.Kind == "DataVolume" - }) - - if len(pvc.OwnerReferences) != len(ownerReferences) { - pvc.OwnerReferences = ownerReferences - err = s.client.Update(ctx, pvc) - if err != nil && !k8serrors.IsNotFound(err) { - return false, fmt.Errorf("update owner ref of pvc: %w", err) - } - } + importSupplementsDeleted, err := s.cleanupPVCImport(ctx, sup, target) + if err != nil { + return false, fmt.Errorf("delete pvc import supplements: %w", err) } - // 2. Delete network policy. - networkPolicy, err := networkpolicy.GetNetworkPolicy(ctx, s.client, sup.LegacyDataVolume(), sup) + networkPolicy, err := networkpolicy.GetNetworkPolicy(ctx, s.client, sup.LegacyCommonResourceName(), sup) if err != nil { return false, err } @@ -271,78 +189,10 @@ func (s DiskService) CleanUpSupplements(ctx context.Context, sup supplements.Gen } } - // 3. Delete DataVolume. - var hasDeleted bool - dv, err := s.GetDataVolume(ctx, sup) - if err != nil { - return false, fmt.Errorf("get dv: %w", err) - } - - if dv != nil { - err = s.protection.RemoveProtection(ctx, dv) - if err != nil { - return false, fmt.Errorf("remove protection from dv: %w", err) - } - - err = s.client.Delete(ctx, dv) - if err != nil && !k8serrors.IsNotFound(err) { - return false, fmt.Errorf("delete dv: %w", err) - } - - hasDeleted = true - } - - return hasDeleted, supplements.CleanupForDataVolume(ctx, s.client, sup, s.dvcrSettings) -} - -func (s DiskService) Protect(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { - err := s.protection.AddOwnerRef(ctx, owner, pvc) - if err != nil { - return fmt.Errorf("failed to add owner ref for pvc: %w", err) - } - - err = s.protection.AddProtection(ctx, dv, pvc) - if err != nil { - return fmt.Errorf("failed to add protection for disk's supplements: %w", err) - } - - if dv != nil { - networkPolicy, err := networkpolicy.GetNetworkPolicyFromObject(ctx, s.client, dv, sup) - if err != nil { - return fmt.Errorf("failed to get networkPolicy for disk's supplements protection: %w", err) - } - - if networkPolicy != nil { - err = s.protection.AddProtection(ctx, networkPolicy) - if err != nil { - return fmt.Errorf("failed to remove protection for disk's supplements: %w", err) - } - } - } - - return nil + return importSupplementsDeleted || networkPolicy != nil, nil } -func (s DiskService) Unprotect(ctx context.Context, sup supplements.Generator, dv *cdiv1.DataVolume) error { - err := s.protection.RemoveProtection(ctx, dv) - if err != nil { - return fmt.Errorf("failed to remove protection for disk's supplements: %w", err) - } - - if dv != nil { - networkPolicy, err := networkpolicy.GetNetworkPolicyFromObject(ctx, s.client, dv, sup) - if err != nil { - return fmt.Errorf("failed to get networkPolicy for removing disk's supplements protection: %w", err) - } - - if networkPolicy != nil { - err = s.protection.RemoveProtection(ctx, networkPolicy) - if err != nil { - return fmt.Errorf("failed to remove protection for disk's supplements: %w", err) - } - } - } - +func (s DiskService) Unprotect(_ context.Context, _ supplements.Generator) error { return nil } @@ -368,27 +218,6 @@ func (s DiskService) Resize(ctx context.Context, pvc *corev1.PersistentVolumeCla return nil } -func (s DiskService) IsImportDone(dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) bool { - return dv != nil && dv.Status.Phase == cdiv1.Succeeded && pvc != nil && pvc.Status.Phase == corev1.ClaimBound -} - -func (s DiskService) GetProgress(dv *cdiv1.DataVolume, prevProgress string, opts ...GetProgressOption) string { - if dv == nil { - return prevProgress - } - - dvProgress := string(dv.Status.Progress) - if dvProgress != "N/A" && dvProgress != "" { - for _, o := range opts { - dvProgress = o.Apply(dvProgress) - } - - return dvProgress - } - - return prevProgress -} - func (s DiskService) GetCapacity(pvc *corev1.PersistentVolumeClaim) string { if pvc != nil && pvc.Status.Phase == corev1.ClaimBound { return ptr.To(pvc.Status.Capacity[corev1.ResourceStorage]).String() @@ -401,21 +230,10 @@ func (s DiskService) GetStorageProfile(ctx context.Context, name string) (*cdiv1 return object.FetchObject(ctx, types.NamespacedName{Name: name}, s.client, &cdiv1.StorageProfile{}) } -func (s DiskService) isImmediateBindingMode(sc *storagev1.StorageClass) bool { - if sc == nil { - return false - } - return sc.GetAnnotations()[annotations.AnnVirtualDiskBindingMode] == string(storagev1.VolumeBindingImmediate) -} - func (s DiskService) GetStorageClass(ctx context.Context, scName string) (*storagev1.StorageClass, error) { return object.FetchObject(ctx, types.NamespacedName{Name: scName}, s.client, &storagev1.StorageClass{}) } -func (s DiskService) GetDataVolume(ctx context.Context, sup supplements.Generator) (*cdiv1.DataVolume, error) { - return supplements.FetchSupplement(ctx, s.client, sup, supplements.SupplementDataVolume, &cdiv1.DataVolume{}) -} - func (s DiskService) GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { return supplements.FetchSupplement(ctx, s.client, sup, supplements.SupplementPVC, &corev1.PersistentVolumeClaim{}) } @@ -448,49 +266,6 @@ func (s DiskService) GetVirtualDiskSnapshot(ctx context.Context, name, namespace return object.FetchObject(ctx, types.NamespacedName{Name: name, Namespace: namespace}, s.client, &v1alpha2.VirtualDiskSnapshot{}) } -func (s DiskService) CheckImportProcess(ctx context.Context, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { - if dv == nil { - return nil - } - - dvRunning := GetDataVolumeCondition(cdiv1.DataVolumeRunning, dv.Status.Conditions) - if dvRunning == nil || dvRunning.Status != corev1.ConditionFalse { - return nil - } - - if strings.Contains(dvRunning.Reason, "Error") { - return fmt.Errorf("%w: %s", ErrDataVolumeNotRunning, dvRunning.Message) - } - - if pvc == nil { - return nil - } - - key := types.NamespacedName{ - Namespace: dv.Namespace, - Name: dvutil.GetImporterPrimeName(pvc.UID), - } - - cdiImporterPrime, err := object.FetchObject(ctx, key, s.client, &corev1.Pod{}) - if err != nil { - return err - } - - if cdiImporterPrime != nil { - podInitializedCond, ok := conditions.GetPodCondition(corev1.PodInitialized, cdiImporterPrime.Status.Conditions) - if ok && podInitializedCond.Status == corev1.ConditionFalse && strings.Contains(podInitializedCond.Reason, "Error") { - return fmt.Errorf("%w; %s error %s: %s", ErrDataVolumeNotRunning, key.String(), podInitializedCond.Reason, podInitializedCond.Message) - } - - podScheduledCond, ok := conditions.GetPodCondition(corev1.PodScheduled, cdiImporterPrime.Status.Conditions) - if ok && podScheduledCond.Status == corev1.ConditionFalse && strings.Contains(podScheduledCond.Reason, "Error") { - return fmt.Errorf("%w; %s error %s: %s", ErrDataVolumeNotRunning, key.String(), podScheduledCond.Reason, podScheduledCond.Message) - } - } - - return nil -} - var ErrInsufficientPVCSize = errors.New("the specified pvc size is insufficient") func GetValidatedPVCSize(pvcSize *resource.Quantity, requiredSize resource.Quantity) (resource.Quantity, error) { diff --git a/images/virtualization-artifact/pkg/controller/service/errors.go b/images/virtualization-artifact/pkg/controller/service/errors.go index 7842a7f31d..c08e91da65 100644 --- a/images/virtualization-artifact/pkg/controller/service/errors.go +++ b/images/virtualization-artifact/pkg/controller/service/errors.go @@ -22,9 +22,9 @@ import ( ) var ( - ErrDefaultStorageClassNotFound = errors.New("default storage class not found") - ErrDataVolumeNotRunning = errors.New("pvc importer is not running") - ErrDataVolumeProvisionerUnschedulable = errors.New("provisioner unschedulable") + ErrDefaultStorageClassNotFound = errors.New("default storage class not found") + ErrImporterNotRunning = errors.New("pvc importer is not running") + ErrProvisionerUnschedulable = errors.New("provisioner unschedulable") ) type NoSizingPolicyMatchError struct { diff --git a/images/virtualization-artifact/pkg/controller/service/stat_service.go b/images/virtualization-artifact/pkg/controller/service/stat_service.go index da86d95b24..1eb210f067 100644 --- a/images/virtualization-artifact/pkg/controller/service/stat_service.go +++ b/images/virtualization-artifact/pkg/controller/service/stat_service.go @@ -283,6 +283,21 @@ func (s StatService) IsUploaderReady(pod *corev1.Pod, svc *corev1.Service, ing * return false, nil } + if svc.Spec.ClusterIP != "" { + client := &http.Client{Timeout: 5 * time.Second} + response, err := client.Get(fmt.Sprintf("http://%s/upload", svc.Spec.ClusterIP)) + if err != nil { + return false, nil + } + defer response.Body.Close() + + if response.StatusCode == http.StatusOK { + return true, nil + } + + return false, nil + } + uploadURL, ok := ing.Annotations[annotations.AnnUploadURL] if ok && uploadURL != "" { certPool, err := x509.SystemCertPool() diff --git a/images/virtualization-artifact/pkg/controller/storageprofile/storageprofile_controller.go b/images/virtualization-artifact/pkg/controller/storageprofile/storageprofile_controller.go new file mode 100644 index 0000000000..19331236da --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/storageprofile/storageprofile_controller.go @@ -0,0 +1,248 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storageprofile + +import ( + "context" + "fmt" + "reflect" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/logger" +) + +const ControllerName = "storageprofile-controller" + +type Reconciler struct { + client client.Client + log *log.Logger +} + +func NewController(mgr manager.Manager, log *log.Logger) (controller.Controller, error) { + reconciler := &Reconciler{client: mgr.GetClient(), log: log} + ctr, err := controller.New(ControllerName, mgr, controller.Options{ + Reconciler: reconciler, + LogConstructor: logger.NewConstructor(log), + }) + if err != nil { + return nil, err + } + if err := addWatches(mgr, ctr); err != nil { + return nil, err + } + log.Info("Initialized StorageProfile controller") + return ctr, nil +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + sc := &storagev1.StorageClass{} + if err := r.client.Get(ctx, req.NamespacedName, sc); err != nil { + if k8serrors.IsNotFound(err) { + return reconcile.Result{}, r.deleteStorageProfile(ctx, req.Name) + } + return reconcile.Result{}, err + } + if sc.DeletionTimestamp != nil { + return reconcile.Result{}, r.deleteStorageProfile(ctx, req.Name) + } + return reconcile.Result{}, r.reconcileStorageProfile(ctx, sc) +} + +func (r *Reconciler) reconcileStorageProfile(ctx context.Context, sc *storagev1.StorageClass) error { + profile := &cdiv1.StorageProfile{} + var previous *cdiv1.StorageProfile + if err := r.client.Get(ctx, types.NamespacedName{Name: sc.Name}, profile); err != nil { + if !k8serrors.IsNotFound(err) { + return err + } + profile = emptyStorageProfile(sc.Name) + } else { + previous = profile.DeepCopy() + } + + profile.Status.StorageClass = &sc.Name + profile.Status.Provisioner = &sc.Provisioner + snapshotClass := r.snapshotClassForProvisioner(ctx, sc.Provisioner, profile.Spec.SnapshotClass) + if snapshotClass == "" { + profile.Status.SnapshotClass = nil + } else { + profile.Status.SnapshotClass = &snapshotClass + } + profile.Status.CloneStrategy = reconcileCloneStrategy(sc, profile.Spec.CloneStrategy, snapshotClass) + profile.Status.DataImportCronSourceFormat = reconcileDataImportCronSourceFormat(profile.Spec.DataImportCronSourceFormat) + if len(profile.Spec.ClaimPropertySets) > 0 { + profile.Status.ClaimPropertySets = profile.Spec.ClaimPropertySets + } else { + profile.Status.ClaimPropertySets = defaultClaimPropertySets() + } + + if previous == nil { + return r.client.Create(ctx, profile) + } + if !reflect.DeepEqual(previous, profile) { + return r.client.Update(ctx, profile) + } + return nil +} + +func (r *Reconciler) deleteStorageProfile(ctx context.Context, name string) error { + err := r.client.Delete(ctx, &cdiv1.StorageProfile{ObjectMeta: metav1.ObjectMeta{Name: name}}) + if err != nil && !k8serrors.IsNotFound(err) { + return err + } + return nil +} + +func (r *Reconciler) snapshotClassForProvisioner(ctx context.Context, provisioner string, desired *string) string { + var list vsv1.VolumeSnapshotClassList + if err := r.client.List(ctx, &list); err != nil { + return "" + } + if desired != nil && *desired != "" { + for _, item := range list.Items { + if item.Name == *desired && item.Driver == provisioner { + return item.Name + } + } + return "" + } + for _, item := range list.Items { + if item.Driver == provisioner { + return item.Name + } + } + return "" +} + +func reconcileCloneStrategy(sc *storagev1.StorageClass, desired *cdiv1.CDICloneStrategy, snapshotClass string) *cdiv1.CDICloneStrategy { + if desired != nil { + return desired + } + if value, ok := sc.Annotations["cdi.kubevirt.io/clone-strategy"]; ok { + switch value { + case "copy": + strategy := cdiv1.CloneStrategyHostAssisted + return &strategy + case "snapshot": + strategy := cdiv1.CloneStrategySnapshot + return &strategy + case "csi-clone": + strategy := cdiv1.CloneStrategyCsiClone + return &strategy + } + } + if snapshotClass != "" { + strategy := cdiv1.CloneStrategySnapshot + return &strategy + } + strategy := cdiv1.CloneStrategyHostAssisted + return &strategy +} + +func reconcileDataImportCronSourceFormat(desired *cdiv1.DataImportCronSourceFormat) *cdiv1.DataImportCronSourceFormat { + if desired != nil { + return desired + } + format := cdiv1.DataImportCronSourceFormatPvc + return &format +} + +func defaultClaimPropertySets() []cdiv1.ClaimPropertySet { + fs := corev1.PersistentVolumeFilesystem + block := corev1.PersistentVolumeBlock + return []cdiv1.ClaimPropertySet{ + {AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, VolumeMode: &fs}, + {AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, VolumeMode: &block}, + } +} + +func emptyStorageProfile(name string) *cdiv1.StorageProfile { + return &cdiv1.StorageProfile{ + TypeMeta: metav1.TypeMeta{Kind: "StorageProfile", APIVersion: "cdi.kubevirt.io/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "virtualization-controller", + }, + }, + } +} + +func addWatches(mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{}, &handler.TypedEnqueueRequestForObject[*storagev1.StorageClass]{})); err != nil { + return err + } + if err := ctr.Watch(source.Kind(mgr.GetCache(), &cdiv1.StorageProfile{}, &handler.TypedEnqueueRequestForObject[*cdiv1.StorageProfile]{})); err != nil { + return err + } + if err := ctr.Watch(source.Kind(mgr.GetCache(), &corev1.PersistentVolume{}, + handler.TypedEnqueueRequestsFromMapFunc(func(_ context.Context, pv *corev1.PersistentVolume) []reconcile.Request { + if pv.Spec.StorageClassName == "" { + return nil + } + return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: pv.Spec.StorageClassName}}} + }), + predicate.TypedFuncs[*corev1.PersistentVolume]{ + CreateFunc: func(e event.TypedCreateEvent[*corev1.PersistentVolume]) bool { + return e.Object.Spec.StorageClassName != "" && e.Object.Spec.HostPath != nil + }, + UpdateFunc: func(e event.TypedUpdateEvent[*corev1.PersistentVolume]) bool { + return e.ObjectNew.Spec.StorageClassName != "" && e.ObjectNew.Spec.HostPath != nil + }, + DeleteFunc: func(e event.TypedDeleteEvent[*corev1.PersistentVolume]) bool { + return e.Object.Spec.StorageClassName != "" && e.Object.Spec.HostPath != nil + }, + }, + )); err != nil { + return err + } + if err := ctr.Watch(source.Kind(mgr.GetCache(), &vsv1.VolumeSnapshotClass{}, + handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, vsc *vsv1.VolumeSnapshotClass) []reconcile.Request { + var scs storagev1.StorageClassList + if err := mgr.GetClient().List(ctx, &scs); err != nil { + ctr.GetLogger().Error(err, "Unable to list StorageClasses") + return nil + } + var requests []reconcile.Request + for _, sc := range scs.Items { + if sc.Provisioner == vsc.Driver { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: sc.Name}}) + } + } + return requests + }), + )); err != nil { + return fmt.Errorf("watch VolumeSnapshotClass: %w", err) + } + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/storageprofile/storageprofile_controller_test.go b/images/virtualization-artifact/pkg/controller/storageprofile/storageprofile_controller_test.go new file mode 100644 index 0000000000..a924014638 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/storageprofile/storageprofile_controller_test.go @@ -0,0 +1,97 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storageprofile + +import ( + "context" + "testing" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestReconcileStorageProfileCreatesSnapshotStrategy(t *testing.T) { + ctx := context.Background() + scheme := storageProfileTestScheme(t) + sc := &storagev1.StorageClass{Provisioner: "csi.example.com"} + sc.Name = "fast" + vsc := &vsv1.VolumeSnapshotClass{Driver: sc.Provisioner} + vsc.Name = "snap-fast" + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc, vsc).Build() + r := &Reconciler{client: c} + + _, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: sc.Name}}) + if err != nil { + t.Fatalf("reconcile failed: %v", err) + } + + sp := &cdiv1.StorageProfile{} + if err := c.Get(ctx, types.NamespacedName{Name: sc.Name}, sp); err != nil { + t.Fatalf("storageprofile not found: %v", err) + } + if sp.Status.CloneStrategy == nil || *sp.Status.CloneStrategy != cdiv1.CloneStrategySnapshot { + t.Fatalf("unexpected clone strategy: %#v", sp.Status.CloneStrategy) + } + if sp.Status.SnapshotClass == nil || *sp.Status.SnapshotClass != vsc.Name { + t.Fatalf("unexpected snapshot class: %#v", sp.Status.SnapshotClass) + } +} + +func TestReconcileStorageProfileHonorsStorageClassAnnotation(t *testing.T) { + ctx := context.Background() + scheme := storageProfileTestScheme(t) + sc := &storagev1.StorageClass{ + Provisioner: "csi.example.com", + } + sc.Name = "fast" + sc.Annotations = map[string]string{"cdi.kubevirt.io/clone-strategy": "csi-clone"} + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + r := &Reconciler{client: c} + + _, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: sc.Name}}) + if err != nil { + t.Fatalf("reconcile failed: %v", err) + } + + sp := &cdiv1.StorageProfile{} + if err := c.Get(ctx, types.NamespacedName{Name: sc.Name}, sp); err != nil { + t.Fatalf("storageprofile not found: %v", err) + } + if sp.Status.CloneStrategy == nil || *sp.Status.CloneStrategy != cdiv1.CloneStrategyCsiClone { + t.Fatalf("unexpected clone strategy: %#v", sp.Status.CloneStrategy) + } +} + +func storageProfileTestScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + if err := storagev1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := vsv1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := cdiv1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + return scheme +} diff --git a/images/virtualization-artifact/pkg/controller/supplements/ensure.go b/images/virtualization-artifact/pkg/controller/supplements/ensure.go index 8a9a62b0e0..7278182751 100644 --- a/images/virtualization-artifact/pkg/controller/supplements/ensure.go +++ b/images/virtualization-artifact/pkg/controller/supplements/ensure.go @@ -22,15 +22,11 @@ import ( corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - dvutil "github.com/deckhouse/virtualization-controller/pkg/common/datavolume" ingutil "github.com/deckhouse/virtualization-controller/pkg/common/ingress" - "github.com/deckhouse/virtualization-controller/pkg/common/object" podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements/copier" "github.com/deckhouse/virtualization-controller/pkg/dvcr" @@ -127,66 +123,6 @@ func ShouldCopyImagePullSecret(ctrImg *datasource.ContainerRegistry, targetNS st return imgPullNS != "" && imgPullNS != targetNS } -func EnsureForDataVolume(ctx context.Context, client client.Client, supGen DataVolumeSupplement, dv *cdiv1.DataVolume, dvcrSettings *dvcr.Settings) error { - if dvcrSettings.AuthSecret != "" { - authSecret := supGen.DVCRAuthSecretForDV() - authCopier := copier.AuthSecret{ - Secret: copier.Secret{ - Source: types.NamespacedName{ - Name: dvcrSettings.AuthSecret, - Namespace: dvcrSettings.AuthSecretNamespace, - }, - Destination: authSecret, - OwnerReference: dvutil.MakeOwnerReference(dv), - }, - } - - err := authCopier.CopyCDICompatible(ctx, client, dvcrSettings.RegistryURL) - if err != nil { - return err - } - } - - // CABundle needs transformation, so it always copied. - if dvcrSettings.CertsSecret != "" { - caBundleCM := supGen.DVCRCABundleConfigMapForDV() - caBundleCopier := copier.CABundleConfigMap{ - SourceSecret: types.NamespacedName{ - Name: dvcrSettings.CertsSecret, - Namespace: dvcrSettings.CertsSecretNamespace, - }, - Destination: caBundleCM, - OwnerReference: dvutil.MakeOwnerReference(dv), - } - - return caBundleCopier.Copy(ctx, client) - } - - return nil -} - -func CleanupForDataVolume(ctx context.Context, client client.Client, supGen Generator, dvcrSettings *dvcr.Settings) error { - // AuthSecret has type dockerconfigjson and should be transformed, so it always copied. - if dvcrSettings.AuthSecret != "" { - authSecret := supGen.DVCRAuthSecretForDV() - err := object.CleanupByName(ctx, client, authSecret, &corev1.Secret{}) - if err != nil && !k8serrors.IsNotFound(err) { - return err - } - } - - // CABundle needs transformation, so it always copied. - if dvcrSettings.CertsSecret != "" { - caBundleCM := supGen.DVCRCABundleConfigMapForDV() - err := object.CleanupByName(ctx, client, caBundleCM, &corev1.ConfigMap{}) - if err != nil && !k8serrors.IsNotFound(err) { - return err - } - } - - return nil -} - func EnsureForIngress(ctx context.Context, client client.Client, supGen Generator, ing *netv1.Ingress, dvcrSettings *dvcr.Settings) error { if ShouldCopyUploaderTLSSecret(dvcrSettings, supGen) { tlsSecret := supGen.UploaderTLSSecretForIngress() @@ -204,10 +140,3 @@ func EnsureForIngress(ctx context.Context, client client.Client, supGen Generato } return nil } - -type DataVolumeSupplement interface { - DataVolume() types.NamespacedName - DVCRAuthSecretForDV() types.NamespacedName - DVCRCABundleConfigMapForDV() types.NamespacedName - NetworkPolicy() types.NamespacedName -} diff --git a/images/virtualization-artifact/pkg/controller/supplements/fetch.go b/images/virtualization-artifact/pkg/controller/supplements/fetch.go index b20766538b..9b848aa8ae 100644 --- a/images/virtualization-artifact/pkg/controller/supplements/fetch.go +++ b/images/virtualization-artifact/pkg/controller/supplements/fetch.go @@ -40,8 +40,7 @@ const ( SupplementUploaderIngress SupplementType = "UploaderIngress" // Volumes - SupplementPVC SupplementType = "PersistentVolumeClaim" - SupplementDataVolume SupplementType = "DataVolume" + SupplementPVC SupplementType = "PersistentVolumeClaim" // ConfigMaps/Secrets SupplementDVCRAuthSecret SupplementType = "DVCRAuthSecret" @@ -72,8 +71,6 @@ func GetSupplementName(gen Generator, supplementType SupplementType) (types.Name // Volumes case SupplementPVC: return gen.PersistentVolumeClaim(), nil - case SupplementDataVolume: - return gen.DataVolume(), nil // ConfigMaps/Secrets case SupplementDVCRAuthSecret: @@ -114,8 +111,6 @@ func GetLegacySupplementName(gen Generator, supplementType SupplementType) (type // Volumes case SupplementPVC: return gen.LegacyPersistentVolumeClaim(), nil - case SupplementDataVolume: - return gen.LegacyDataVolume(), nil // ConfigMaps/Secrets case SupplementDVCRAuthSecret: diff --git a/images/virtualization-artifact/pkg/controller/supplements/fetch_test.go b/images/virtualization-artifact/pkg/controller/supplements/fetch_test.go index b5f18eba03..d13120cc3a 100644 --- a/images/virtualization-artifact/pkg/controller/supplements/fetch_test.go +++ b/images/virtualization-artifact/pkg/controller/supplements/fetch_test.go @@ -55,7 +55,7 @@ var _ = Describe("FetchSupplement", func() { It("should fetch the resource successfully", func() { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: "d8v-vi-importer-test-image-12345678-1234-1234-1234-123456789abc", + Name: "d8v-vi-importer-12345678-1234-1234-1234-123456789abc", Namespace: "default", }, Spec: corev1.PodSpec{ @@ -115,7 +115,7 @@ var _ = Describe("FetchSupplement", func() { It("should prefer the new naming", func() { newPod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: "d8v-vi-importer-test-image-12345678-1234-1234-1234-123456789abc", + Name: "d8v-vi-importer-12345678-1234-1234-1234-123456789abc", Namespace: "default", Labels: map[string]string{"version": "new"}, }, diff --git a/images/virtualization-artifact/pkg/controller/supplements/generator.go b/images/virtualization-artifact/pkg/controller/supplements/generator.go index 4ebd0e917b..d6c679919c 100644 --- a/images/virtualization-artifact/pkg/controller/supplements/generator.go +++ b/images/virtualization-artifact/pkg/controller/supplements/generator.go @@ -31,7 +31,8 @@ const ( tplDVCRCABundle = "d8v-%s-dvcr-ca-%s-%s" tplCABundle = "d8v-%s-ca-%s-%s" tplImagePullSecret = "d8v-%s-pull-image-%s-%s" - tplImporterPod = "d8v-%s-importer-%s-%s" + tplImporterPod = "d8v-%s-importer-%s" + tplPVCImporterPod = "d8v-%s-pvc-importer-%s" tplBounderPod = "d8v-%s-bounder-%s-%s" tplUploaderPod = "d8v-%s-uploader-%s-%s" tplUploaderTLSSecret = "d8v-%s-tls-%s-%s" @@ -44,10 +45,10 @@ type Generator interface { BounderPod() types.NamespacedName ImporterPod() types.NamespacedName + PVCImporterPod() types.NamespacedName UploaderPod() types.NamespacedName UploaderService() types.NamespacedName UploaderIngress() types.NamespacedName - DataVolume() types.NamespacedName PersistentVolumeClaim() types.NamespacedName CABundleConfigMap() types.NamespacedName DVCRAuthSecret() types.NamespacedName @@ -57,13 +58,14 @@ type Generator interface { ImagePullSecret() types.NamespacedName NetworkPolicy() types.NamespacedName CommonSupplement() types.NamespacedName + CommonResourceName() types.NamespacedName LegacyBounderPod() types.NamespacedName LegacyImporterPod() types.NamespacedName LegacyUploaderPod() types.NamespacedName LegacyUploaderService() types.NamespacedName LegacyUploaderIngress() types.NamespacedName - LegacyDataVolume() types.NamespacedName + LegacyCommonResourceName() types.NamespacedName LegacyPersistentVolumeClaim() types.NamespacedName LegacyCABundleConfigMap() types.NamespacedName LegacyDVCRAuthSecret() types.NamespacedName @@ -116,8 +118,7 @@ func (g *generator) DVCRAuthSecret() types.NamespacedName { return g.generateName(tplDVCRAuthSecret, kvalidation.DNS1123SubdomainMaxLength) } -// DVCRAuthSecretForDV returns name and namespace for auth Secret copy -// compatible with DataVolume: with accessKeyId and secretKey fields. +// DVCRAuthSecretForDV returns name and namespace for CDI-compatible auth Secret copy. func (g *generator) DVCRAuthSecretForDV() types.NamespacedName { return g.generateName(tplDVCRAuthSecretForDV, kvalidation.DNS1123SubdomainMaxLength) } @@ -139,7 +140,23 @@ func (g *generator) ImagePullSecret() types.NamespacedName { // ImporterPod generates name for importer Pod. func (g *generator) ImporterPod() types.NamespacedName { - return g.generateName(tplImporterPod, kvalidation.DNS1123SubdomainMaxLength) + name := fmt.Sprintf(tplImporterPod, g.prefix, g.UID()) + return types.NamespacedName{ + Name: name, + Namespace: g.namespace, + } +} + +// PVCImporterPod generates name for the cdi-importer Pod that imports data +// from DVCR into the target PersistentVolumeClaim. It is intentionally +// distinct from ImporterPod() to avoid colliding with the dvcr-importer Pod +// that runs in the first import phase. +func (g *generator) PVCImporterPod() types.NamespacedName { + name := fmt.Sprintf(tplPVCImporterPod, g.prefix, g.UID()) + return types.NamespacedName{ + Name: name, + Namespace: g.namespace, + } } // BounderPod generates name for bounder Pod. @@ -167,9 +184,8 @@ func (g *generator) UploaderTLSSecretForIngress() types.NamespacedName { return g.generateName(tplUploaderTLSSecret, kvalidation.DNS1123SubdomainMaxLength) } -// DataVolume generates name for underlying DataVolume. -// DataVolume is always one for vmd/vmi, so prefix is used. -func (g *generator) DataVolume() types.NamespacedName { +// CommonResourceName generates the shared resource name used by older resource layouts. +func (g *generator) CommonResourceName() types.NamespacedName { return g.generateName(tplCommon, kvalidation.DNS1123SubdomainMaxLength) } @@ -204,8 +220,7 @@ func (g *generator) LegacyDVCRAuthSecret() types.NamespacedName { return g.shortenNamespaced(name) } -// LegacyDVCRAuthSecretForDV returns old format name for auth Secret copy -// compatible with DataVolume: with accessKeyId and secretKey fields. +// LegacyDVCRAuthSecretForDV returns old format name for CDI-compatible auth Secret copy. func (g *generator) LegacyDVCRAuthSecretForDV() types.NamespacedName { name := fmt.Sprintf("%s-dvcr-auth-dv-%s", g.prefix, g.name) return g.shortenNamespaced(name) @@ -265,14 +280,13 @@ func (g *generator) LegacyUploaderTLSSecretForIngress() types.NamespacedName { return g.shortenNamespaced(name) } -// LegacyDataVolume generates old format name for underlying DataVolume. -// DataVolume is always one for vmd/vmi, so prefix is used. -func (g *generator) LegacyDataVolume() types.NamespacedName { - dvName := fmt.Sprintf("%s-%s-%s", g.prefix, g.name, string(g.uid)) - return g.shortenNamespaced(dvName) +// LegacyCommonResourceName generates the shared resource name used by older resource layouts. +func (g *generator) LegacyCommonResourceName() types.NamespacedName { + name := fmt.Sprintf("%s-%s-%s", g.prefix, g.name, string(g.uid)) + return g.shortenNamespaced(name) } // LegacyPersistentVolumeClaim generates old format name for underlying PersistentVolumeClaim. func (g *generator) LegacyPersistentVolumeClaim() types.NamespacedName { - return g.LegacyDataVolume() + return g.LegacyCommonResourceName() } diff --git a/images/virtualization-artifact/pkg/controller/supplements/generator_test.go b/images/virtualization-artifact/pkg/controller/supplements/generator_test.go index 6df9f79a5c..38d910e98f 100644 --- a/images/virtualization-artifact/pkg/controller/supplements/generator_test.go +++ b/images/virtualization-artifact/pkg/controller/supplements/generator_test.go @@ -48,7 +48,9 @@ var _ = Describe("Generator", func() { Expect(result.Name).To(HavePrefix("d8v-")) Expect(result.Name).To(ContainSubstring(expectedPrefix)) - Expect(result.Name).To(ContainSubstring(name)) + if expectedPrefix != "importer" && expectedPrefix != "pvc-importer" { + Expect(result.Name).To(ContainSubstring(name)) + } Expect(result.Name).To(HaveSuffix(string(uid))) }, Entry("DVCRAuthSecret", func(g Generator) types.NamespacedName { return g.DVCRAuthSecret() }, "dvcr-auth"), @@ -57,12 +59,13 @@ var _ = Describe("Generator", func() { Entry("CABundleConfigMap", func(g Generator) types.NamespacedName { return g.CABundleConfigMap() }, "ca"), Entry("ImagePullSecret", func(g Generator) types.NamespacedName { return g.ImagePullSecret() }, "pull-image"), Entry("ImporterPod", func(g Generator) types.NamespacedName { return g.ImporterPod() }, "importer"), + Entry("PVCImporterPod", func(g Generator) types.NamespacedName { return g.PVCImporterPod() }, "pvc-importer"), Entry("BounderPod", func(g Generator) types.NamespacedName { return g.BounderPod() }, "bounder"), Entry("UploaderPod", func(g Generator) types.NamespacedName { return g.UploaderPod() }, "uploader"), Entry("UploaderService", func(g Generator) types.NamespacedName { return g.UploaderService() }, "vi"), Entry("UploaderIngress", func(g Generator) types.NamespacedName { return g.UploaderIngress() }, "vi"), Entry("UploaderTLSSecret", func(g Generator) types.NamespacedName { return g.UploaderTLSSecretForIngress() }, "tls"), - Entry("DataVolume", func(g Generator) types.NamespacedName { return g.DataVolume() }, "vi"), + Entry("CommonResourceName", func(g Generator) types.NamespacedName { return g.CommonResourceName() }, "vi"), Entry("PersistentVolumeClaim", func(g Generator) types.NamespacedName { return g.PersistentVolumeClaim() }, "vi"), Entry("NetworkPolicy", func(g Generator) types.NamespacedName { return g.NetworkPolicy() }, "vi"), Entry("CommonSupplement", func(g Generator) types.NamespacedName { return g.CommonSupplement() }, "vi"), @@ -84,12 +87,13 @@ var _ = Describe("Generator", func() { Entry("CABundleConfigMap - 253 limit", func(g Generator) types.NamespacedName { return g.CABundleConfigMap() }, kvalidation.DNS1123SubdomainMaxLength), Entry("ImagePullSecret - 253 limit", func(g Generator) types.NamespacedName { return g.ImagePullSecret() }, kvalidation.DNS1123SubdomainMaxLength), Entry("ImporterPod - 253 limit", func(g Generator) types.NamespacedName { return g.ImporterPod() }, kvalidation.DNS1123SubdomainMaxLength), + Entry("PVCImporterPod - 253 limit", func(g Generator) types.NamespacedName { return g.PVCImporterPod() }, kvalidation.DNS1123SubdomainMaxLength), Entry("BounderPod - 253 limit", func(g Generator) types.NamespacedName { return g.BounderPod() }, kvalidation.DNS1123SubdomainMaxLength), Entry("UploaderPod - 253 limit", func(g Generator) types.NamespacedName { return g.UploaderPod() }, kvalidation.DNS1123SubdomainMaxLength), Entry("UploaderService - 63 limit", func(g Generator) types.NamespacedName { return g.UploaderService() }, kvalidation.DNS1123LabelMaxLength), Entry("UploaderIngress - 253 limit", func(g Generator) types.NamespacedName { return g.UploaderIngress() }, kvalidation.DNS1123SubdomainMaxLength), Entry("UploaderTLSSecret - 253 limit", func(g Generator) types.NamespacedName { return g.UploaderTLSSecretForIngress() }, kvalidation.DNS1123SubdomainMaxLength), - Entry("DataVolume - 253 limit", func(g Generator) types.NamespacedName { return g.DataVolume() }, kvalidation.DNS1123SubdomainMaxLength), + Entry("CommonResourceName - 253 limit", func(g Generator) types.NamespacedName { return g.CommonResourceName() }, kvalidation.DNS1123SubdomainMaxLength), Entry("PersistentVolumeClaim - 253 limit", func(g Generator) types.NamespacedName { return g.PersistentVolumeClaim() }, kvalidation.DNS1123SubdomainMaxLength), Entry("NetworkPolicy - 253 limit", func(g Generator) types.NamespacedName { return g.NetworkPolicy() }, kvalidation.DNS1123SubdomainMaxLength), Entry("CommonSupplement - 253 limit", func(g Generator) types.NamespacedName { return g.CommonSupplement() }, kvalidation.DNS1123SubdomainMaxLength), diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/blank_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/blank_test.go index be858ba5c5..604481ded5 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/blank_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/blank_test.go @@ -29,7 +29,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" @@ -76,9 +75,6 @@ var _ = Describe("Blank", func() { CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, - ProtectFunc: func(_ context.Context, _ supplements.Generator, _ client.Object, _ *cdiv1.DataVolume, _ *corev1.PersistentVolumeClaim) error { - return nil - }, } sc = &storagev1.StorageClass{ diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go index 696902d2f9..83c0a15362 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go @@ -18,29 +18,17 @@ package source import ( "context" - "errors" "fmt" - "time" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/deckhouse/virtualization-controller/pkg/common" - "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" - "github.com/deckhouse/virtualization-controller/pkg/common/object" - podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" - "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/common/steptaker" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/importer" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" @@ -52,9 +40,9 @@ import ( const httpDataSource = "http" type HTTPDataSource struct { - statService *service.StatService - importerService *service.ImporterService - diskService *service.DiskService + statService HTTPDataSourceStatService + importerService HTTPDataSourceImporterService + diskService HTTPDataSourceDiskService dvcrSettings *dvcr.Settings client client.Client recorder eventrecord.EventRecorderLogger @@ -62,9 +50,9 @@ type HTTPDataSource struct { func NewHTTPDataSource( recorder eventrecord.EventRecorderLogger, - statService *service.StatService, - importerService *service.ImporterService, - diskService *service.DiskService, + statService HTTPDataSourceStatService, + importerService HTTPDataSourceImporterService, + diskService HTTPDataSourceDiskService, dvcrSettings *dvcr.Settings, client client.Client, ) *HTTPDataSource { @@ -81,268 +69,34 @@ func NewHTTPDataSource( func (ds HTTPDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { log, ctx := logger.GetDataSourceContext(ctx, httpDataSource) - condition, _ := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + supgen := vdsupplements.NewGenerator(vd) + cb := conditions.NewConditionBuilder(vdcondition.ReadyType).Generation(vd.Generation) defer func() { conditions.SetCondition(cb, &vd.Status.Conditions) }() - supgen := vdsupplements.NewGenerator(vd) pod, err := ds.importerService.GetPod(ctx, supgen) if err != nil { - return reconcile.Result{}, err - } - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) - if err != nil { - return reconcile.Result{}, err + return reconcile.Result{}, fmt.Errorf("fetch importer pod: %w", err) } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vd.Status.StorageClassName) + pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { - return reconcile.Result{}, err + return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) } - - switch { - case IsDiskProvisioningFinished(condition): - log.Debug("Disk provisioning finished: clean up") - - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) - - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vd, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - // Unprotect import time supplements to delete them later. - err = ds.importerService.Unprotect(ctx, pod, supgen) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, supgen, dv) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vd, ds) - case object.AnyTerminating(pod, dv, pvc): - log.Info("Waiting for supplements to be terminated") - case pod == nil: - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncStarted, - "The HTTP DataSource import to DVCR has started", - ) - - vd.Status.Progress = "0%" - - envSettings := ds.getEnvSettings(vd, supgen) - - err = ds.importerService.Start( - ctx, envSettings, vd, supgen, - datasource.NewCABundleForVMD(vd.GetNamespace(), vd.Spec.DataSource), - service.WithSystemNodeToleration(), - ) - switch { - case err == nil: - // OK. - case common.ErrQuotaExceeded(err): - ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil - default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, err - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case !podutil.IsPodComplete(pod): - log.Info("Provisioning to DVCR is in progress", "podPhase", pod.Status.Phase) - - err = ds.statService.CheckPod(pod) - if err != nil { - return reconcile.Result{}, setPhaseConditionFromPodError(ctx, err, pod, vd, cb, ds.client) - } - - err = ds.importerService.Protect(ctx, pod, supgen) - if err != nil { - return reconcile.Result{}, err - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to DVCR.") - - vd.Status.Progress = ds.statService.GetProgress(vd.GetUID(), pod, vd.Status.Progress, service.NewScaleOption(0, 50)) - vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) - case dv == nil: - if isStorageClassWFFC(sc) && len(vd.Status.AttachedToVirtualMachines) != 1 { - vd.Status.Progress = "50%" - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - return reconcile.Result{}, nil - } - - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncStarted, - "The HTTP DataSource import to PVC has started", - ) - - err = ds.statService.CheckPod(pod) - if err != nil { - vd.Status.Phase = v1alpha2.DiskFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - vd.Status.Progress = "50%" - vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) - - if imageformat.IsISO(ds.statService.GetFormat(pod)) { - setPhaseConditionToFailed(cb, &vd.Status.Phase, ErrISOSourceNotSupported) - return reconcile.Result{}, nil - } - - var diskSize resource.Quantity - diskSize, err = ds.getPVCSize(vd, pod) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) - - if errors.Is(err, service.ErrInsufficientPVCSize) { - return reconcile.Result{}, nil - } - - return reconcile.Result{}, err - } - - source := ds.getSource(supgen, ds.statService.GetDVCRImageName(pod)) - - var nodePlacement *provisioner.NodePlacement - nodePlacement, err = getNodePlacement(ctx, ds.client, vd) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, fmt.Errorf("failed to get importer tolerations: %w", err) - } - - err = ds.diskService.Start(ctx, diskSize, sc, source, vd, supgen, service.WithNodePlacement(nodePlacement)) - if updated, err := setPhaseConditionFromStorageError(err, vd, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vd.Status.Phase = v1alpha2.DiskPending - if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vd.Status.Phase = v1alpha2.DiskPending - if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vd, corev1.EventTypeWarning, vdcondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The HTTP DataSource import has completed", - ) - - vd.Status.Phase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") - - vd.Status.Progress = "100%" - vd.Status.Capacity = ds.diskService.GetCapacity(pvc) - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - err = ds.diskService.CheckProvisioning(ctx, pvc) - if err != nil { - return reconcile.Result{}, setPhaseConditionFromProvisioningError(ctx, err, cb, vd, dv, ds.diskService, ds.client) - } - - vd.Status.Progress = ds.diskService.GetProgress(dv, vd.Status.Progress, service.NewScaleOption(50, 100)) - vd.Status.Capacity = ds.diskService.GetCapacity(pvc) - - err = ds.diskService.Protect(ctx, supgen, vd, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, ptr.Deref(pvc.Spec.StorageClassName, "")) - if err != nil { - return reconcile.Result{}, err - } - - if err = setPhaseConditionForPVCProvisioningDisk(ctx, dv, vd, pvc, sc, cb, ds.diskService); err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + if pvc != nil { + ctx = logger.ToContext(ctx, log.With("pvc.name", pvc.Name, "pvc.status.phase", pvc.Status.Phase)) } - return reconcile.Result{RequeueAfter: time.Second}, nil + return steptaker.NewStepTakers[*v1alpha2.VirtualDisk]( + step.NewCleanUpImporterStep(pod, ds.importerService), + step.NewReadyStep(ds.diskService, pvc, cb), + step.NewTerminatingStep(pvc), + step.NewCreateImporterStep(pvc, pod, ds.buildEnvSettings, ds.importerService, ds.recorder, cb, "The HTTP DataSource import to DVCR has started"), + step.NewWaitForDVCRImporterStep(pod, ds.statService, ds.importerService, ds.client, cb), + step.NewPVCImportFromDVCRStep(pvc, pod, ds.statService, ds.diskService, ds.client, ds.recorder, cb, "The HTTP DataSource import to PVC has started"), + step.NewWaitForPVCStep(pvc, ds.client, cb), + step.NewWaitForPVCImportStep(pvc, step.DVCRPodPVCImportSource(pod, ds.statService), ds.diskService, ds.statService, service.NewScaleOption(50, 100), ds.client, cb), + ).Run(ctx, vd) } func (ds HTTPDataSource) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDisk) (bool, error) { @@ -350,12 +104,12 @@ func (ds HTTPDataSource) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDisk) importerRequeue, err := ds.importerService.CleanUp(ctx, supgen) if err != nil { - return false, err + return false, fmt.Errorf("clean up importer: %w", err) } diskRequeue, err := ds.diskService.CleanUp(ctx, supgen) if err != nil { - return false, err + return false, fmt.Errorf("clean up disk: %w", err) } return importerRequeue || diskRequeue, nil @@ -365,31 +119,11 @@ func (ds HTTPDataSource) Validate(_ context.Context, _ *v1alpha2.VirtualDisk) er return nil } -func (ds HTTPDataSource) CleanUpSupplements(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { - supgen := vdsupplements.NewGenerator(vd) - - importerRequeue, err := ds.importerService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - diskRequeue, err := ds.diskService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - if importerRequeue || diskRequeue { - return reconcile.Result{RequeueAfter: time.Second}, nil - } else { - return reconcile.Result{}, nil - } -} - func (ds HTTPDataSource) Name() string { return httpDataSource } -func (ds HTTPDataSource) getEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *importer.Settings { +func (ds HTTPDataSource) buildEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *importer.Settings { var settings importer.Settings importer.ApplyHTTPSourceSettings(&settings, vd.Spec.DataSource.HTTP, supgen) @@ -402,30 +136,3 @@ func (ds HTTPDataSource) getEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplem return &settings } - -func (ds HTTPDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *cdiv1.DataVolumeSource { - // The image was preloaded from source into dvcr. - // We can't use the same data source a second time, but we can set dvcr as the data source. - // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. - url := common.DockerRegistrySchemePrefix + dvcrSourceImageName - secretName := sup.DVCRAuthSecretForDV().Name - certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } -} - -func (ds HTTPDataSource) getPVCSize(vd *v1alpha2.VirtualDisk, pod *corev1.Pod) (resource.Quantity, error) { - // Get size from the importer Pod to detect if specified PVC size is enough. - unpackedSize, err := resource.ParseQuantity(ds.statService.GetSize(pod).UnpackedBytes) - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", ds.statService.GetSize(pod).UnpackedBytes, err) - } - - return service.GetValidatedPVCSize(vd.Spec.PersistentVolumeClaim.Size, unpackedSize) -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/http_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/http_test.go new file mode 100644 index 0000000000..f9c438b8fd --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/http_test.go @@ -0,0 +1,413 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "errors" + "log/slog" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +var _ = Describe("HTTPDataSource", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vd *v1alpha2.VirtualDisk + sc *storagev1.StorageClass + pvc *corev1.PersistentVolumeClaim + disk *HTTPDataSourceDiskServiceMock + importerSvc *HTTPDataSourceImporterServiceMock + stat *HTTPDataSourceStatServiceMock + recorder eventrecord.EventRecorderLogger + dvcrSettings *dvcr.Settings + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(v1alpha2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(storagev1.AddToScheme(scheme)).To(Succeed()) + + recorder = &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, _, _ string) {}, + } + + dvcrSettings = &dvcr.Settings{ + RegistryURL: "dvcr.example.com", + } + + sc = &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{Name: "sc"}, + } + + vd = &v1alpha2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vd", + Generation: 1, + UID: "22222222-2222-2222-2222-222222222222", + }, + Spec: v1alpha2.VirtualDiskSpec{ + DataSource: &v1alpha2.VirtualDiskDataSource{ + Type: v1alpha2.DataSourceTypeHTTP, + HTTP: &v1alpha2.DataSourceHTTP{URL: "https://example.com/image.qcow2"}, + }, + }, + Status: v1alpha2.VirtualDiskStatus{ + StorageClassName: sc.Name, + Target: v1alpha2.DiskTarget{PersistentVolumeClaim: "test-pvc"}, + }, + } + + supgen := vdsupplements.NewGenerator(vd) + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.PersistentVolumeClaim().Name, + Namespace: vd.Namespace, + }, + Spec: corev1.PersistentVolumeClaimSpec{StorageClassName: ptr.To(sc.Name)}, + Status: corev1.PersistentVolumeClaimStatus{ + Capacity: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("1Gi")}, + }, + } + + disk = &HTTPDataSourceDiskServiceMock{ + GetCapacityFunc: func(_ *corev1.PersistentVolumeClaim) string { return "1Gi" }, + GetPersistentVolumeClaimFunc: func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return pvc, nil + }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + } + + importerSvc = &HTTPDataSourceImporterServiceMock{ + GetPodFunc: func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return nil, nil }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + ProtectFunc: func(_ context.Context, _ *corev1.Pod, _ supplements.Generator) error { return nil }, + } + + stat = &HTTPDataSourceStatServiceMock{ + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { return "dvcr.example.com/cvi/vd:1" }, + GetSizeFunc: func(_ *corev1.Pod) v1alpha2.ImageStatusSize { + return v1alpha2.ImageStatusSize{UnpackedBytes: "500Mi"} + }, + GetFormatFunc: func(_ *corev1.Pod) string { return "qcow2" }, + GetDownloadSpeedFunc: func(_ types.UID, _ *corev1.Pod) *v1alpha2.StatusSpeed { return nil }, + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, prev string, _ ...service.GetProgressOption) string { + if prev == "" { + return "10%" + } + return prev + }, + CheckPodFunc: func(_ *corev1.Pod) error { return nil }, + } + }) + + newSyncer := func(c client.Client) *HTTPDataSource { + return NewHTTPDataSource(recorder, stat, importerSvc, disk, dvcrSettings, c) + } + + Context("VirtualDisk has just been created (no importer pod yet)", func() { + It("starts the importer pod and sets DiskProvisioning", func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + var started bool + var gotSettings *importer.Settings + importerSvc.StartFunc = func(_ context.Context, settings *importer.Settings, _ client.Object, _ supplements.Generator, _ *datasource.CABundle, _ ...service.Option) error { + started = true + gotSettings = settings + return nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(started).To(BeTrue()) + Expect(gotSettings).ToNot(BeNil()) + + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + Expect(vd.Status.Progress).To(Equal("0%")) + }) + + It("propagates QuotaExceeded as DiskFailed/QuotaExceeded", func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + importerSvc.StartFunc = func(_ context.Context, _ *importer.Settings, _ client.Object, _ supplements.Generator, _ *datasource.CABundle, _ ...service.Option) error { + return errors.New("exceeded quota: cpu requested but limit reached") + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.QuotaExceeded, true) + }) + }) + + Context("Importer pod is running", func() { + var pod *corev1.Pod + + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("marks Provisioning and updates progress/download speed", func() { + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(vd.Status.Progress).ToNot(BeEmpty()) + Expect(importerSvc.ProtectCalls()).To(HaveLen(1)) + }) + + It("surfaces ProvisioningFailed when CheckPod returns ErrProvisioningFailed", func() { + stat.CheckPodFunc = func(_ *corev1.Pod) error { return service.ErrProvisioningFailed } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.ProvisioningFailed, true) + }) + }) + + Context("Importer pod completed, no PVC yet", func() { + var pod *corev1.Pod + + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("kicks off the PVC import", func() { + var started bool + disk.StartPVCImportFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, source *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) error { + started = true + Expect(source).ToNot(BeNil()) + Expect(source.Registry).ToNot(BeNil()) + return nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(started).To(BeTrue()) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + }) + + It("fails the disk when the source is ISO", func() { + stat.GetFormatFunc = func(_ *corev1.Pod) string { return "iso" } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.ProvisioningFailed, true) + }) + }) + + Context("PVC is created but not yet Bound", func() { + var pod *corev1.Pod + + BeforeEach(func() { + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("reports WaitForFirstConsumer for WFFC storage class", func() { + pvc.Status.Phase = corev1.ClaimPending + sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingWaitForFirstConsumer) + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskWaitForFirstConsumer)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.WaitingForFirstConsumer, true) + }) + + It("reports Provisioning while waiting for Bound", func() { + pvc.Status.Phase = corev1.ClaimPending + sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingImmediate) + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + }) + }) + + Context("PVC is Bound and the import is still in flight", func() { + BeforeEach(func() { + pvc.Status.Phase = corev1.ClaimBound + pvc.Annotations = map[string]string{annotations.AnnPVCImportPhase: string(corev1.PodRunning)} + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("drives EnsurePVCImport with a registry source built from the importer pod", func() { + disk.EnsurePVCImportFunc = func(_ context.Context, _ *corev1.PersistentVolumeClaim, source *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) (corev1.PodPhase, error) { + Expect(source).ToNot(BeNil()) + Expect(source.Registry).ToNot(BeNil()) + return corev1.PodRunning, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + // The step requeues every 2s to refresh vd.Status.Progress from + // the cdi-importer pod metrics while the import is in flight. + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + }) + + It("requeues when EnsurePVCImport reports Succeeded", func() { + disk.EnsurePVCImportFunc = func(_ context.Context, _ *corev1.PersistentVolumeClaim, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) (corev1.PodPhase, error) { + return corev1.PodSucceeded, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + }) + }) + + Context("PVC is Bound and the import is complete", func() { + BeforeEach(func() { + pvc.Status.Phase = corev1.ClaimBound + pvc.Annotations = map[string]string{annotations.AnnPVCImportPhase: string(corev1.PodSucceeded)} + }) + + It("marks DiskReady and cleans up the importer once the condition is finished", func() { + vd.Status.Conditions = []metav1.Condition{{ + Type: vdcondition.ReadyType.String(), + Reason: vdcondition.Ready.String(), + Status: metav1.ConditionTrue, + }} + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + var cleaned bool + importerSvc.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + cleaned = true + return true, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(cleaned).To(BeTrue()) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskReady)) + ExpectCondition(vd, metav1.ConditionTrue, vdcondition.Ready, false) + ExpectStats(vd) + }) + }) + + Context("CleanUp", func() { + It("delegates to both importer and disk services", func() { + var importerCleaned, diskCleaned bool + importerSvc.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + importerCleaned = true + return true, nil + } + disk.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + diskCleaned = true + return false, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + requeue, err := newSyncer(cl).CleanUp(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(requeue).To(BeTrue()) + Expect(importerCleaned).To(BeTrue()) + Expect(diskCleaned).To(BeTrue()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/interfaces.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/interfaces.go index cd26fd373e..b4e59ba160 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/interfaces.go @@ -19,6 +19,8 @@ package source import ( "context" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" @@ -26,7 +28,7 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2" ) -//go:generate go tool moq -rm -out mock.go . Handler BlankDataSourceDiskService ObjectRefVirtualImageDiskService ObjectRefClusterVirtualImageDiskService ObjectRefVirtualDiskSnapshotDiskService +//go:generate go tool moq -rm -out mock.go . Handler BlankDataSourceDiskService ObjectRefVirtualImageDiskService ObjectRefVirtualImageStatService ObjectRefClusterVirtualImageDiskService ObjectRefClusterVirtualImageStatService ObjectRefVirtualDiskSnapshotDiskService UploadDataSourceDiskService UploadDataSourceUploaderService UploadDataSourceStatService HTTPDataSourceDiskService HTTPDataSourceImporterService HTTPDataSourceStatService RegistryDataSourceDiskService RegistryDataSourceImporterService RegistryDataSourceStatService type Handler interface { Name() string @@ -44,18 +46,95 @@ type BlankDataSourceDiskService interface { type ObjectRefVirtualImageDiskService interface { step.ReadyStepDiskService - step.WaitForDVStepDiskService - step.CreateDataVolumeStepDiskService - step.EnsureNodePlacementStepDiskService + step.PVCImportStepDiskService + step.WaitForPVCImportStepDiskService +} + +type ObjectRefVirtualImageStatService interface { + step.WaitForPVCImportStepStatService } type ObjectRefClusterVirtualImageDiskService interface { step.ReadyStepDiskService - step.WaitForDVStepDiskService - step.CreateDataVolumeStepDiskService - step.EnsureNodePlacementStepDiskService + step.PVCImportStepDiskService + step.WaitForPVCImportStepDiskService +} + +type ObjectRefClusterVirtualImageStatService interface { + step.WaitForPVCImportStepStatService } type ObjectRefVirtualDiskSnapshotDiskService interface { step.ReadyStepDiskService } + +type UploadDataSourceDiskService interface { + step.ReadyStepDiskService + step.PVCImportStepDiskService + step.WaitForPVCImportStepDiskService + + GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) + CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) +} + +type UploadDataSourceUploaderService interface { + step.CreateUploaderStepUploaderService + step.WaitForUserUploadStepUploaderService + step.CleanUpUploaderStepUploaderService + + GetPod(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) + GetService(ctx context.Context, sup supplements.Generator) (*corev1.Service, error) + GetIngress(ctx context.Context, sup supplements.Generator) (*netv1.Ingress, error) +} + +type UploadDataSourceStatService interface { + step.WaitForUserUploadStepStatService + step.PVCImportFromDVCRStepStatService + step.WaitForPVCImportStepStatService +} + +type HTTPDataSourceDiskService interface { + step.ReadyStepDiskService + step.PVCImportStepDiskService + step.WaitForPVCImportStepDiskService + + GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) + CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) +} + +type HTTPDataSourceImporterService interface { + step.CreateImporterStepImporterService + step.WaitForDVCRImporterStepImporterService + step.CleanUpImporterStepImporterService + + GetPod(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) +} + +type HTTPDataSourceStatService interface { + step.WaitForDVCRImporterStepStatService + step.PVCImportFromDVCRStepStatService + step.WaitForPVCImportStepStatService +} + +type RegistryDataSourceDiskService interface { + step.ReadyStepDiskService + step.PVCImportStepDiskService + step.WaitForPVCImportStepDiskService + + GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) + CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) +} + +type RegistryDataSourceImporterService interface { + step.CreateImporterStepImporterService + step.WaitForDVCRImporterStepImporterService + step.CleanUpImporterStepImporterService + + GetPod(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) +} + +type RegistryDataSourceStatService interface { + step.WaitForDVCRImporterStepStatService + step.PVCImportFromDVCRStepStatService + step.WaitForPVCImportStepStatService +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/mock.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/mock.go index 7e1e92e2e9..b97f332543 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/mock.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/mock.go @@ -5,13 +5,18 @@ package source import ( "context" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" "github.com/deckhouse/virtualization/api/core/v1alpha2" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" + netv1 "k8s.io/api/networking/v1" + "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sync" @@ -245,12 +250,9 @@ var _ BlankDataSourceDiskService = &BlankDataSourceDiskServiceMock{} // GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { // panic("mock out the GetCapacity method") // }, -// GetVolumeAndAccessModesFunc: func(ctx context.Context, obj client.Object, sc *storagev1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) { +// GetVolumeAndAccessModesFunc: func(ctx context.Context, obj client.Object, sc *v1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) { // panic("mock out the GetVolumeAndAccessModes method") // }, -// ProtectFunc: func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { -// panic("mock out the Protect method") -// }, // } // // // use mockedBlankDataSourceDiskService in code that requires BlankDataSourceDiskService @@ -268,10 +270,7 @@ type BlankDataSourceDiskServiceMock struct { GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string // GetVolumeAndAccessModesFunc mocks the GetVolumeAndAccessModes method. - GetVolumeAndAccessModesFunc func(ctx context.Context, obj client.Object, sc *storagev1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) - - // ProtectFunc mocks the Protect method. - ProtectFunc func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error + GetVolumeAndAccessModesFunc func(ctx context.Context, obj client.Object, sc *v1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) // calls tracks calls to the methods. calls struct { @@ -301,27 +300,13 @@ type BlankDataSourceDiskServiceMock struct { // Obj is the obj argument value. Obj client.Object // Sc is the sc argument value. - Sc *storagev1.StorageClass - } - // Protect holds details about calls to the Protect method. - Protect []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Sup is the sup argument value. - Sup supplements.Generator - // Owner is the owner argument value. - Owner client.Object - // Dv is the dv argument value. - Dv *cdiv1.DataVolume - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim + Sc *v1.StorageClass } } lockCleanUp sync.RWMutex lockCleanUpSupplements sync.RWMutex lockGetCapacity sync.RWMutex lockGetVolumeAndAccessModes sync.RWMutex - lockProtect sync.RWMutex } // CleanUp calls CleanUpFunc. @@ -429,14 +414,14 @@ func (mock *BlankDataSourceDiskServiceMock) GetCapacityCalls() []struct { } // GetVolumeAndAccessModes calls GetVolumeAndAccessModesFunc. -func (mock *BlankDataSourceDiskServiceMock) GetVolumeAndAccessModes(ctx context.Context, obj client.Object, sc *storagev1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) { +func (mock *BlankDataSourceDiskServiceMock) GetVolumeAndAccessModes(ctx context.Context, obj client.Object, sc *v1.StorageClass) (corev1.PersistentVolumeMode, corev1.PersistentVolumeAccessMode, error) { if mock.GetVolumeAndAccessModesFunc == nil { panic("BlankDataSourceDiskServiceMock.GetVolumeAndAccessModesFunc: method is nil but BlankDataSourceDiskService.GetVolumeAndAccessModes was just called") } callInfo := struct { Ctx context.Context Obj client.Object - Sc *storagev1.StorageClass + Sc *v1.StorageClass }{ Ctx: ctx, Obj: obj, @@ -455,12 +440,12 @@ func (mock *BlankDataSourceDiskServiceMock) GetVolumeAndAccessModes(ctx context. func (mock *BlankDataSourceDiskServiceMock) GetVolumeAndAccessModesCalls() []struct { Ctx context.Context Obj client.Object - Sc *storagev1.StorageClass + Sc *v1.StorageClass } { var calls []struct { Ctx context.Context Obj client.Object - Sc *storagev1.StorageClass + Sc *v1.StorageClass } mock.lockGetVolumeAndAccessModes.RLock() calls = mock.calls.GetVolumeAndAccessModes @@ -468,54 +453,6 @@ func (mock *BlankDataSourceDiskServiceMock) GetVolumeAndAccessModesCalls() []str return calls } -// Protect calls ProtectFunc. -func (mock *BlankDataSourceDiskServiceMock) Protect(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { - if mock.ProtectFunc == nil { - panic("BlankDataSourceDiskServiceMock.ProtectFunc: method is nil but BlankDataSourceDiskService.Protect was just called") - } - callInfo := struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim - }{ - Ctx: ctx, - Sup: sup, - Owner: owner, - Dv: dv, - Pvc: pvc, - } - mock.lockProtect.Lock() - mock.calls.Protect = append(mock.calls.Protect, callInfo) - mock.lockProtect.Unlock() - return mock.ProtectFunc(ctx, sup, owner, dv, pvc) -} - -// ProtectCalls gets all the calls that were made to Protect. -// Check the length with: -// -// len(mockedBlankDataSourceDiskService.ProtectCalls()) -func (mock *BlankDataSourceDiskServiceMock) ProtectCalls() []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim -} { - var calls []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim - } - mock.lockProtect.RLock() - calls = mock.calls.Protect - mock.lockProtect.RUnlock() - return calls -} - // Ensure, that ObjectRefVirtualImageDiskServiceMock does implement ObjectRefVirtualImageDiskService. // If this is not the case, regenerate this file with moq. var _ ObjectRefVirtualImageDiskService = &ObjectRefVirtualImageDiskServiceMock{} @@ -526,26 +463,17 @@ var _ ObjectRefVirtualImageDiskService = &ObjectRefVirtualImageDiskServiceMock{} // // // make and configure a mocked ObjectRefVirtualImageDiskService // mockedObjectRefVirtualImageDiskService := &ObjectRefVirtualImageDiskServiceMock{ -// CheckProvisioningFunc: func(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { -// panic("mock out the CheckProvisioning method") -// }, -// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { -// panic("mock out the CleanUp method") -// }, // CleanUpSupplementsFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { // panic("mock out the CleanUpSupplements method") // }, +// EnsurePVCImportFunc: func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { +// panic("mock out the EnsurePVCImport method") +// }, // GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { // panic("mock out the GetCapacity method") // }, -// GetProgressFunc: func(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string { -// panic("mock out the GetProgress method") -// }, -// ProtectFunc: func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { -// panic("mock out the Protect method") -// }, -// StartFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error { -// panic("mock out the Start method") +// StartPVCImportFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { +// panic("mock out the StartPVCImport method") // }, // } // @@ -554,174 +482,65 @@ var _ ObjectRefVirtualImageDiskService = &ObjectRefVirtualImageDiskServiceMock{} // // } type ObjectRefVirtualImageDiskServiceMock struct { - // CheckProvisioningFunc mocks the CheckProvisioning method. - CheckProvisioningFunc func(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error - - // CleanUpFunc mocks the CleanUp method. - CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) - // CleanUpSupplementsFunc mocks the CleanUpSupplements method. CleanUpSupplementsFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + // EnsurePVCImportFunc mocks the EnsurePVCImport method. + EnsurePVCImportFunc func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) + // GetCapacityFunc mocks the GetCapacity method. GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string - // GetProgressFunc mocks the GetProgress method. - GetProgressFunc func(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string - - // ProtectFunc mocks the Protect method. - ProtectFunc func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error - - // StartFunc mocks the Start method. - StartFunc func(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error + // StartPVCImportFunc mocks the StartPVCImport method. + StartPVCImportFunc func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error // calls tracks calls to the methods. calls struct { - // CheckProvisioning holds details about calls to the CheckProvisioning method. - CheckProvisioning []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim - } - // CleanUp holds details about calls to the CleanUp method. - CleanUp []struct { + // CleanUpSupplements holds details about calls to the CleanUpSupplements method. + CleanUpSupplements []struct { // Ctx is the ctx argument value. Ctx context.Context // Sup is the sup argument value. Sup supplements.Generator } - // CleanUpSupplements holds details about calls to the CleanUpSupplements method. - CleanUpSupplements []struct { + // EnsurePVCImport holds details about calls to the EnsurePVCImport method. + EnsurePVCImport []struct { // Ctx is the ctx argument value. Ctx context.Context - // Sup is the sup argument value. - Sup supplements.Generator + // Target is the target argument value. + Target *corev1.PersistentVolumeClaim + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement } // GetCapacity holds details about calls to the GetCapacity method. GetCapacity []struct { // Pvc is the pvc argument value. Pvc *corev1.PersistentVolumeClaim } - // GetProgress holds details about calls to the GetProgress method. - GetProgress []struct { - // Dv is the dv argument value. - Dv *cdiv1.DataVolume - // PrevProgress is the prevProgress argument value. - PrevProgress string - // Opts is the opts argument value. - Opts []service.GetProgressOption - } - // Protect holds details about calls to the Protect method. - Protect []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Sup is the sup argument value. - Sup supplements.Generator - // Owner is the owner argument value. - Owner client.Object - // Dv is the dv argument value. - Dv *cdiv1.DataVolume - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim - } - // Start holds details about calls to the Start method. - Start []struct { + // StartPVCImport holds details about calls to the StartPVCImport method. + StartPVCImport []struct { // Ctx is the ctx argument value. Ctx context.Context // PvcSize is the pvcSize argument value. PvcSize resource.Quantity // Sc is the sc argument value. - Sc *storagev1.StorageClass + Sc *v1.StorageClass // Source is the source argument value. - Source *cdiv1.DataVolumeSource - // Obj is the obj argument value. - Obj client.Object - // Sup is the sup argument value. - Sup supplements.DataVolumeSupplement - // Opts is the opts argument value. - Opts []service.Option + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement } } - lockCheckProvisioning sync.RWMutex - lockCleanUp sync.RWMutex lockCleanUpSupplements sync.RWMutex + lockEnsurePVCImport sync.RWMutex lockGetCapacity sync.RWMutex - lockGetProgress sync.RWMutex - lockProtect sync.RWMutex - lockStart sync.RWMutex -} - -// CheckProvisioning calls CheckProvisioningFunc. -func (mock *ObjectRefVirtualImageDiskServiceMock) CheckProvisioning(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { - if mock.CheckProvisioningFunc == nil { - panic("ObjectRefVirtualImageDiskServiceMock.CheckProvisioningFunc: method is nil but ObjectRefVirtualImageDiskService.CheckProvisioning was just called") - } - callInfo := struct { - Ctx context.Context - Pvc *corev1.PersistentVolumeClaim - }{ - Ctx: ctx, - Pvc: pvc, - } - mock.lockCheckProvisioning.Lock() - mock.calls.CheckProvisioning = append(mock.calls.CheckProvisioning, callInfo) - mock.lockCheckProvisioning.Unlock() - return mock.CheckProvisioningFunc(ctx, pvc) -} - -// CheckProvisioningCalls gets all the calls that were made to CheckProvisioning. -// Check the length with: -// -// len(mockedObjectRefVirtualImageDiskService.CheckProvisioningCalls()) -func (mock *ObjectRefVirtualImageDiskServiceMock) CheckProvisioningCalls() []struct { - Ctx context.Context - Pvc *corev1.PersistentVolumeClaim -} { - var calls []struct { - Ctx context.Context - Pvc *corev1.PersistentVolumeClaim - } - mock.lockCheckProvisioning.RLock() - calls = mock.calls.CheckProvisioning - mock.lockCheckProvisioning.RUnlock() - return calls -} - -// CleanUp calls CleanUpFunc. -func (mock *ObjectRefVirtualImageDiskServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { - if mock.CleanUpFunc == nil { - panic("ObjectRefVirtualImageDiskServiceMock.CleanUpFunc: method is nil but ObjectRefVirtualImageDiskService.CleanUp was just called") - } - callInfo := struct { - Ctx context.Context - Sup supplements.Generator - }{ - Ctx: ctx, - Sup: sup, - } - mock.lockCleanUp.Lock() - mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) - mock.lockCleanUp.Unlock() - return mock.CleanUpFunc(ctx, sup) -} - -// CleanUpCalls gets all the calls that were made to CleanUp. -// Check the length with: -// -// len(mockedObjectRefVirtualImageDiskService.CleanUpCalls()) -func (mock *ObjectRefVirtualImageDiskServiceMock) CleanUpCalls() []struct { - Ctx context.Context - Sup supplements.Generator -} { - var calls []struct { - Ctx context.Context - Sup supplements.Generator - } - mock.lockCleanUp.RLock() - calls = mock.calls.CleanUp - mock.lockCleanUp.RUnlock() - return calls + lockStartPVCImport sync.RWMutex } // CleanUpSupplements calls CleanUpSupplementsFunc. @@ -760,6 +579,54 @@ func (mock *ObjectRefVirtualImageDiskServiceMock) CleanUpSupplementsCalls() []st return calls } +// EnsurePVCImport calls EnsurePVCImportFunc. +func (mock *ObjectRefVirtualImageDiskServiceMock) EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + if mock.EnsurePVCImportFunc == nil { + panic("ObjectRefVirtualImageDiskServiceMock.EnsurePVCImportFunc: method is nil but ObjectRefVirtualImageDiskService.EnsurePVCImport was just called") + } + callInfo := struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + Target: target, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockEnsurePVCImport.Lock() + mock.calls.EnsurePVCImport = append(mock.calls.EnsurePVCImport, callInfo) + mock.lockEnsurePVCImport.Unlock() + return mock.EnsurePVCImportFunc(ctx, target, source, vd, nodePlacement) +} + +// EnsurePVCImportCalls gets all the calls that were made to EnsurePVCImport. +// Check the length with: +// +// len(mockedObjectRefVirtualImageDiskService.EnsurePVCImportCalls()) +func (mock *ObjectRefVirtualImageDiskServiceMock) EnsurePVCImportCalls() []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockEnsurePVCImport.RLock() + calls = mock.calls.EnsurePVCImport + mock.lockEnsurePVCImport.RUnlock() + return calls +} + // GetCapacity calls GetCapacityFunc. func (mock *ObjectRefVirtualImageDiskServiceMock) GetCapacity(pvc *corev1.PersistentVolumeClaim) string { if mock.GetCapacityFunc == nil { @@ -792,147 +659,139 @@ func (mock *ObjectRefVirtualImageDiskServiceMock) GetCapacityCalls() []struct { return calls } -// GetProgress calls GetProgressFunc. -func (mock *ObjectRefVirtualImageDiskServiceMock) GetProgress(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string { - if mock.GetProgressFunc == nil { - panic("ObjectRefVirtualImageDiskServiceMock.GetProgressFunc: method is nil but ObjectRefVirtualImageDiskService.GetProgress was just called") +// StartPVCImport calls StartPVCImportFunc. +func (mock *ObjectRefVirtualImageDiskServiceMock) StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + if mock.StartPVCImportFunc == nil { + panic("ObjectRefVirtualImageDiskServiceMock.StartPVCImportFunc: method is nil but ObjectRefVirtualImageDiskService.StartPVCImport was just called") } callInfo := struct { - Dv *cdiv1.DataVolume - PrevProgress string - Opts []service.GetProgressOption + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement }{ - Dv: dv, - PrevProgress: prevProgress, - Opts: opts, + Ctx: ctx, + PvcSize: pvcSize, + Sc: sc, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, } - mock.lockGetProgress.Lock() - mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) - mock.lockGetProgress.Unlock() - return mock.GetProgressFunc(dv, prevProgress, opts...) + mock.lockStartPVCImport.Lock() + mock.calls.StartPVCImport = append(mock.calls.StartPVCImport, callInfo) + mock.lockStartPVCImport.Unlock() + return mock.StartPVCImportFunc(ctx, pvcSize, sc, source, vd, nodePlacement) } -// GetProgressCalls gets all the calls that were made to GetProgress. +// StartPVCImportCalls gets all the calls that were made to StartPVCImport. // Check the length with: // -// len(mockedObjectRefVirtualImageDiskService.GetProgressCalls()) -func (mock *ObjectRefVirtualImageDiskServiceMock) GetProgressCalls() []struct { - Dv *cdiv1.DataVolume - PrevProgress string - Opts []service.GetProgressOption +// len(mockedObjectRefVirtualImageDiskService.StartPVCImportCalls()) +func (mock *ObjectRefVirtualImageDiskServiceMock) StartPVCImportCalls() []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement } { var calls []struct { - Dv *cdiv1.DataVolume - PrevProgress string - Opts []service.GetProgressOption + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement } - mock.lockGetProgress.RLock() - calls = mock.calls.GetProgress - mock.lockGetProgress.RUnlock() + mock.lockStartPVCImport.RLock() + calls = mock.calls.StartPVCImport + mock.lockStartPVCImport.RUnlock() return calls } -// Protect calls ProtectFunc. -func (mock *ObjectRefVirtualImageDiskServiceMock) Protect(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { - if mock.ProtectFunc == nil { - panic("ObjectRefVirtualImageDiskServiceMock.ProtectFunc: method is nil but ObjectRefVirtualImageDiskService.Protect was just called") - } - callInfo := struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim - }{ - Ctx: ctx, - Sup: sup, - Owner: owner, - Dv: dv, - Pvc: pvc, - } - mock.lockProtect.Lock() - mock.calls.Protect = append(mock.calls.Protect, callInfo) - mock.lockProtect.Unlock() - return mock.ProtectFunc(ctx, sup, owner, dv, pvc) -} +// Ensure, that ObjectRefVirtualImageStatServiceMock does implement ObjectRefVirtualImageStatService. +// If this is not the case, regenerate this file with moq. +var _ ObjectRefVirtualImageStatService = &ObjectRefVirtualImageStatServiceMock{} -// ProtectCalls gets all the calls that were made to Protect. -// Check the length with: +// ObjectRefVirtualImageStatServiceMock is a mock implementation of ObjectRefVirtualImageStatService. // -// len(mockedObjectRefVirtualImageDiskService.ProtectCalls()) -func (mock *ObjectRefVirtualImageDiskServiceMock) ProtectCalls() []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim -} { - var calls []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim +// func TestSomethingThatUsesObjectRefVirtualImageStatService(t *testing.T) { +// +// // make and configure a mocked ObjectRefVirtualImageStatService +// mockedObjectRefVirtualImageStatService := &ObjectRefVirtualImageStatServiceMock{ +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// } +// +// // use mockedObjectRefVirtualImageStatService in code that requires ObjectRefVirtualImageStatService +// // and then make assertions. +// +// } +type ObjectRefVirtualImageStatServiceMock struct { + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + + // calls tracks calls to the methods. + calls struct { + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } } - mock.lockProtect.RLock() - calls = mock.calls.Protect - mock.lockProtect.RUnlock() - return calls + lockGetProgress sync.RWMutex } -// Start calls StartFunc. -func (mock *ObjectRefVirtualImageDiskServiceMock) Start(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error { - if mock.StartFunc == nil { - panic("ObjectRefVirtualImageDiskServiceMock.StartFunc: method is nil but ObjectRefVirtualImageDiskService.Start was just called") +// GetProgress calls GetProgressFunc. +func (mock *ObjectRefVirtualImageStatServiceMock) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("ObjectRefVirtualImageStatServiceMock.GetProgressFunc: method is nil but ObjectRefVirtualImageStatService.GetProgress was just called") } callInfo := struct { - Ctx context.Context - PvcSize resource.Quantity - Sc *storagev1.StorageClass - Source *cdiv1.DataVolumeSource - Obj client.Object - Sup supplements.DataVolumeSupplement - Opts []service.Option + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption }{ - Ctx: ctx, - PvcSize: pvcSize, - Sc: sc, - Source: source, - Obj: obj, - Sup: sup, - Opts: opts, + OwnerUID: ownerUID, + Pod: pod, + PrevProgress: prevProgress, + Opts: opts, } - mock.lockStart.Lock() - mock.calls.Start = append(mock.calls.Start, callInfo) - mock.lockStart.Unlock() - return mock.StartFunc(ctx, pvcSize, sc, source, obj, sup, opts...) + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(ownerUID, pod, prevProgress, opts...) } -// StartCalls gets all the calls that were made to Start. +// GetProgressCalls gets all the calls that were made to GetProgress. // Check the length with: // -// len(mockedObjectRefVirtualImageDiskService.StartCalls()) -func (mock *ObjectRefVirtualImageDiskServiceMock) StartCalls() []struct { - Ctx context.Context - PvcSize resource.Quantity - Sc *storagev1.StorageClass - Source *cdiv1.DataVolumeSource - Obj client.Object - Sup supplements.DataVolumeSupplement - Opts []service.Option +// len(mockedObjectRefVirtualImageStatService.GetProgressCalls()) +func (mock *ObjectRefVirtualImageStatServiceMock) GetProgressCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption } { var calls []struct { - Ctx context.Context - PvcSize resource.Quantity - Sc *storagev1.StorageClass - Source *cdiv1.DataVolumeSource - Obj client.Object - Sup supplements.DataVolumeSupplement - Opts []service.Option + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption } - mock.lockStart.RLock() - calls = mock.calls.Start - mock.lockStart.RUnlock() + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() return calls } @@ -946,26 +805,17 @@ var _ ObjectRefClusterVirtualImageDiskService = &ObjectRefClusterVirtualImageDis // // // make and configure a mocked ObjectRefClusterVirtualImageDiskService // mockedObjectRefClusterVirtualImageDiskService := &ObjectRefClusterVirtualImageDiskServiceMock{ -// CheckProvisioningFunc: func(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { -// panic("mock out the CheckProvisioning method") -// }, -// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { -// panic("mock out the CleanUp method") -// }, // CleanUpSupplementsFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { // panic("mock out the CleanUpSupplements method") // }, +// EnsurePVCImportFunc: func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { +// panic("mock out the EnsurePVCImport method") +// }, // GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { // panic("mock out the GetCapacity method") // }, -// GetProgressFunc: func(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string { -// panic("mock out the GetProgress method") -// }, -// ProtectFunc: func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { -// panic("mock out the Protect method") -// }, -// StartFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error { -// panic("mock out the Start method") +// StartPVCImportFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { +// panic("mock out the StartPVCImport method") // }, // } // @@ -974,174 +824,65 @@ var _ ObjectRefClusterVirtualImageDiskService = &ObjectRefClusterVirtualImageDis // // } type ObjectRefClusterVirtualImageDiskServiceMock struct { - // CheckProvisioningFunc mocks the CheckProvisioning method. - CheckProvisioningFunc func(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error - - // CleanUpFunc mocks the CleanUp method. - CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) - // CleanUpSupplementsFunc mocks the CleanUpSupplements method. CleanUpSupplementsFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + // EnsurePVCImportFunc mocks the EnsurePVCImport method. + EnsurePVCImportFunc func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) + // GetCapacityFunc mocks the GetCapacity method. GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string - // GetProgressFunc mocks the GetProgress method. - GetProgressFunc func(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string - - // ProtectFunc mocks the Protect method. - ProtectFunc func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error - - // StartFunc mocks the Start method. - StartFunc func(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error + // StartPVCImportFunc mocks the StartPVCImport method. + StartPVCImportFunc func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error // calls tracks calls to the methods. calls struct { - // CheckProvisioning holds details about calls to the CheckProvisioning method. - CheckProvisioning []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim - } - // CleanUp holds details about calls to the CleanUp method. - CleanUp []struct { + // CleanUpSupplements holds details about calls to the CleanUpSupplements method. + CleanUpSupplements []struct { // Ctx is the ctx argument value. Ctx context.Context // Sup is the sup argument value. Sup supplements.Generator } - // CleanUpSupplements holds details about calls to the CleanUpSupplements method. - CleanUpSupplements []struct { + // EnsurePVCImport holds details about calls to the EnsurePVCImport method. + EnsurePVCImport []struct { // Ctx is the ctx argument value. Ctx context.Context - // Sup is the sup argument value. - Sup supplements.Generator + // Target is the target argument value. + Target *corev1.PersistentVolumeClaim + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement } // GetCapacity holds details about calls to the GetCapacity method. GetCapacity []struct { // Pvc is the pvc argument value. Pvc *corev1.PersistentVolumeClaim } - // GetProgress holds details about calls to the GetProgress method. - GetProgress []struct { - // Dv is the dv argument value. - Dv *cdiv1.DataVolume - // PrevProgress is the prevProgress argument value. - PrevProgress string - // Opts is the opts argument value. - Opts []service.GetProgressOption - } - // Protect holds details about calls to the Protect method. - Protect []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Sup is the sup argument value. - Sup supplements.Generator - // Owner is the owner argument value. - Owner client.Object - // Dv is the dv argument value. - Dv *cdiv1.DataVolume - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim - } - // Start holds details about calls to the Start method. - Start []struct { + // StartPVCImport holds details about calls to the StartPVCImport method. + StartPVCImport []struct { // Ctx is the ctx argument value. Ctx context.Context // PvcSize is the pvcSize argument value. PvcSize resource.Quantity // Sc is the sc argument value. - Sc *storagev1.StorageClass + Sc *v1.StorageClass // Source is the source argument value. - Source *cdiv1.DataVolumeSource - // Obj is the obj argument value. - Obj client.Object - // Sup is the sup argument value. - Sup supplements.DataVolumeSupplement - // Opts is the opts argument value. - Opts []service.Option + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement } } - lockCheckProvisioning sync.RWMutex - lockCleanUp sync.RWMutex lockCleanUpSupplements sync.RWMutex + lockEnsurePVCImport sync.RWMutex lockGetCapacity sync.RWMutex - lockGetProgress sync.RWMutex - lockProtect sync.RWMutex - lockStart sync.RWMutex -} - -// CheckProvisioning calls CheckProvisioningFunc. -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) CheckProvisioning(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { - if mock.CheckProvisioningFunc == nil { - panic("ObjectRefClusterVirtualImageDiskServiceMock.CheckProvisioningFunc: method is nil but ObjectRefClusterVirtualImageDiskService.CheckProvisioning was just called") - } - callInfo := struct { - Ctx context.Context - Pvc *corev1.PersistentVolumeClaim - }{ - Ctx: ctx, - Pvc: pvc, - } - mock.lockCheckProvisioning.Lock() - mock.calls.CheckProvisioning = append(mock.calls.CheckProvisioning, callInfo) - mock.lockCheckProvisioning.Unlock() - return mock.CheckProvisioningFunc(ctx, pvc) -} - -// CheckProvisioningCalls gets all the calls that were made to CheckProvisioning. -// Check the length with: -// -// len(mockedObjectRefClusterVirtualImageDiskService.CheckProvisioningCalls()) -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) CheckProvisioningCalls() []struct { - Ctx context.Context - Pvc *corev1.PersistentVolumeClaim -} { - var calls []struct { - Ctx context.Context - Pvc *corev1.PersistentVolumeClaim - } - mock.lockCheckProvisioning.RLock() - calls = mock.calls.CheckProvisioning - mock.lockCheckProvisioning.RUnlock() - return calls -} - -// CleanUp calls CleanUpFunc. -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { - if mock.CleanUpFunc == nil { - panic("ObjectRefClusterVirtualImageDiskServiceMock.CleanUpFunc: method is nil but ObjectRefClusterVirtualImageDiskService.CleanUp was just called") - } - callInfo := struct { - Ctx context.Context - Sup supplements.Generator - }{ - Ctx: ctx, - Sup: sup, - } - mock.lockCleanUp.Lock() - mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) - mock.lockCleanUp.Unlock() - return mock.CleanUpFunc(ctx, sup) -} - -// CleanUpCalls gets all the calls that were made to CleanUp. -// Check the length with: -// -// len(mockedObjectRefClusterVirtualImageDiskService.CleanUpCalls()) -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) CleanUpCalls() []struct { - Ctx context.Context - Sup supplements.Generator -} { - var calls []struct { - Ctx context.Context - Sup supplements.Generator - } - mock.lockCleanUp.RLock() - calls = mock.calls.CleanUp - mock.lockCleanUp.RUnlock() - return calls + lockStartPVCImport sync.RWMutex } // CleanUpSupplements calls CleanUpSupplementsFunc. @@ -1180,6 +921,54 @@ func (mock *ObjectRefClusterVirtualImageDiskServiceMock) CleanUpSupplementsCalls return calls } +// EnsurePVCImport calls EnsurePVCImportFunc. +func (mock *ObjectRefClusterVirtualImageDiskServiceMock) EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + if mock.EnsurePVCImportFunc == nil { + panic("ObjectRefClusterVirtualImageDiskServiceMock.EnsurePVCImportFunc: method is nil but ObjectRefClusterVirtualImageDiskService.EnsurePVCImport was just called") + } + callInfo := struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + Target: target, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockEnsurePVCImport.Lock() + mock.calls.EnsurePVCImport = append(mock.calls.EnsurePVCImport, callInfo) + mock.lockEnsurePVCImport.Unlock() + return mock.EnsurePVCImportFunc(ctx, target, source, vd, nodePlacement) +} + +// EnsurePVCImportCalls gets all the calls that were made to EnsurePVCImport. +// Check the length with: +// +// len(mockedObjectRefClusterVirtualImageDiskService.EnsurePVCImportCalls()) +func (mock *ObjectRefClusterVirtualImageDiskServiceMock) EnsurePVCImportCalls() []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockEnsurePVCImport.RLock() + calls = mock.calls.EnsurePVCImport + mock.lockEnsurePVCImport.RUnlock() + return calls +} + // GetCapacity calls GetCapacityFunc. func (mock *ObjectRefClusterVirtualImageDiskServiceMock) GetCapacity(pvc *corev1.PersistentVolumeClaim) string { if mock.GetCapacityFunc == nil { @@ -1212,147 +1001,139 @@ func (mock *ObjectRefClusterVirtualImageDiskServiceMock) GetCapacityCalls() []st return calls } -// GetProgress calls GetProgressFunc. -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) GetProgress(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string { - if mock.GetProgressFunc == nil { - panic("ObjectRefClusterVirtualImageDiskServiceMock.GetProgressFunc: method is nil but ObjectRefClusterVirtualImageDiskService.GetProgress was just called") +// StartPVCImport calls StartPVCImportFunc. +func (mock *ObjectRefClusterVirtualImageDiskServiceMock) StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + if mock.StartPVCImportFunc == nil { + panic("ObjectRefClusterVirtualImageDiskServiceMock.StartPVCImportFunc: method is nil but ObjectRefClusterVirtualImageDiskService.StartPVCImport was just called") } callInfo := struct { - Dv *cdiv1.DataVolume - PrevProgress string - Opts []service.GetProgressOption + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement }{ - Dv: dv, - PrevProgress: prevProgress, - Opts: opts, + Ctx: ctx, + PvcSize: pvcSize, + Sc: sc, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, } - mock.lockGetProgress.Lock() - mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) - mock.lockGetProgress.Unlock() - return mock.GetProgressFunc(dv, prevProgress, opts...) + mock.lockStartPVCImport.Lock() + mock.calls.StartPVCImport = append(mock.calls.StartPVCImport, callInfo) + mock.lockStartPVCImport.Unlock() + return mock.StartPVCImportFunc(ctx, pvcSize, sc, source, vd, nodePlacement) } -// GetProgressCalls gets all the calls that were made to GetProgress. +// StartPVCImportCalls gets all the calls that were made to StartPVCImport. // Check the length with: // -// len(mockedObjectRefClusterVirtualImageDiskService.GetProgressCalls()) -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) GetProgressCalls() []struct { - Dv *cdiv1.DataVolume - PrevProgress string - Opts []service.GetProgressOption +// len(mockedObjectRefClusterVirtualImageDiskService.StartPVCImportCalls()) +func (mock *ObjectRefClusterVirtualImageDiskServiceMock) StartPVCImportCalls() []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement } { var calls []struct { - Dv *cdiv1.DataVolume - PrevProgress string - Opts []service.GetProgressOption + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement } - mock.lockGetProgress.RLock() - calls = mock.calls.GetProgress - mock.lockGetProgress.RUnlock() + mock.lockStartPVCImport.RLock() + calls = mock.calls.StartPVCImport + mock.lockStartPVCImport.RUnlock() return calls } -// Protect calls ProtectFunc. -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) Protect(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { - if mock.ProtectFunc == nil { - panic("ObjectRefClusterVirtualImageDiskServiceMock.ProtectFunc: method is nil but ObjectRefClusterVirtualImageDiskService.Protect was just called") - } - callInfo := struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim - }{ - Ctx: ctx, - Sup: sup, - Owner: owner, - Dv: dv, - Pvc: pvc, - } - mock.lockProtect.Lock() - mock.calls.Protect = append(mock.calls.Protect, callInfo) - mock.lockProtect.Unlock() - return mock.ProtectFunc(ctx, sup, owner, dv, pvc) -} +// Ensure, that ObjectRefClusterVirtualImageStatServiceMock does implement ObjectRefClusterVirtualImageStatService. +// If this is not the case, regenerate this file with moq. +var _ ObjectRefClusterVirtualImageStatService = &ObjectRefClusterVirtualImageStatServiceMock{} -// ProtectCalls gets all the calls that were made to Protect. -// Check the length with: +// ObjectRefClusterVirtualImageStatServiceMock is a mock implementation of ObjectRefClusterVirtualImageStatService. // -// len(mockedObjectRefClusterVirtualImageDiskService.ProtectCalls()) -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) ProtectCalls() []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim -} { - var calls []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim +// func TestSomethingThatUsesObjectRefClusterVirtualImageStatService(t *testing.T) { +// +// // make and configure a mocked ObjectRefClusterVirtualImageStatService +// mockedObjectRefClusterVirtualImageStatService := &ObjectRefClusterVirtualImageStatServiceMock{ +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// } +// +// // use mockedObjectRefClusterVirtualImageStatService in code that requires ObjectRefClusterVirtualImageStatService +// // and then make assertions. +// +// } +type ObjectRefClusterVirtualImageStatServiceMock struct { + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + + // calls tracks calls to the methods. + calls struct { + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } } - mock.lockProtect.RLock() - calls = mock.calls.Protect - mock.lockProtect.RUnlock() - return calls + lockGetProgress sync.RWMutex } -// Start calls StartFunc. -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) Start(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error { - if mock.StartFunc == nil { - panic("ObjectRefClusterVirtualImageDiskServiceMock.StartFunc: method is nil but ObjectRefClusterVirtualImageDiskService.Start was just called") +// GetProgress calls GetProgressFunc. +func (mock *ObjectRefClusterVirtualImageStatServiceMock) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("ObjectRefClusterVirtualImageStatServiceMock.GetProgressFunc: method is nil but ObjectRefClusterVirtualImageStatService.GetProgress was just called") } callInfo := struct { - Ctx context.Context - PvcSize resource.Quantity - Sc *storagev1.StorageClass - Source *cdiv1.DataVolumeSource - Obj client.Object - Sup supplements.DataVolumeSupplement - Opts []service.Option + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption }{ - Ctx: ctx, - PvcSize: pvcSize, - Sc: sc, - Source: source, - Obj: obj, - Sup: sup, - Opts: opts, + OwnerUID: ownerUID, + Pod: pod, + PrevProgress: prevProgress, + Opts: opts, } - mock.lockStart.Lock() - mock.calls.Start = append(mock.calls.Start, callInfo) - mock.lockStart.Unlock() - return mock.StartFunc(ctx, pvcSize, sc, source, obj, sup, opts...) + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(ownerUID, pod, prevProgress, opts...) } -// StartCalls gets all the calls that were made to Start. +// GetProgressCalls gets all the calls that were made to GetProgress. // Check the length with: // -// len(mockedObjectRefClusterVirtualImageDiskService.StartCalls()) -func (mock *ObjectRefClusterVirtualImageDiskServiceMock) StartCalls() []struct { - Ctx context.Context - PvcSize resource.Quantity - Sc *storagev1.StorageClass - Source *cdiv1.DataVolumeSource - Obj client.Object - Sup supplements.DataVolumeSupplement - Opts []service.Option +// len(mockedObjectRefClusterVirtualImageStatService.GetProgressCalls()) +func (mock *ObjectRefClusterVirtualImageStatServiceMock) GetProgressCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption } { var calls []struct { - Ctx context.Context - PvcSize resource.Quantity - Sc *storagev1.StorageClass - Source *cdiv1.DataVolumeSource - Obj client.Object - Sup supplements.DataVolumeSupplement - Opts []service.Option + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption } - mock.lockStart.RLock() - calls = mock.calls.Start - mock.lockStart.RUnlock() + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() return calls } @@ -1372,9 +1153,6 @@ var _ ObjectRefVirtualDiskSnapshotDiskService = &ObjectRefVirtualDiskSnapshotDis // GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { // panic("mock out the GetCapacity method") // }, -// ProtectFunc: func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { -// panic("mock out the Protect method") -// }, // } // // // use mockedObjectRefVirtualDiskSnapshotDiskService in code that requires ObjectRefVirtualDiskSnapshotDiskService @@ -1388,9 +1166,6 @@ type ObjectRefVirtualDiskSnapshotDiskServiceMock struct { // GetCapacityFunc mocks the GetCapacity method. GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string - // ProtectFunc mocks the Protect method. - ProtectFunc func(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error - // calls tracks calls to the methods. calls struct { // CleanUpSupplements holds details about calls to the CleanUpSupplements method. @@ -1405,23 +1180,9 @@ type ObjectRefVirtualDiskSnapshotDiskServiceMock struct { // Pvc is the pvc argument value. Pvc *corev1.PersistentVolumeClaim } - // Protect holds details about calls to the Protect method. - Protect []struct { - // Ctx is the ctx argument value. - Ctx context.Context - // Sup is the sup argument value. - Sup supplements.Generator - // Owner is the owner argument value. - Owner client.Object - // Dv is the dv argument value. - Dv *cdiv1.DataVolume - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim - } } lockCleanUpSupplements sync.RWMutex lockGetCapacity sync.RWMutex - lockProtect sync.RWMutex } // CleanUpSupplements calls CleanUpSupplementsFunc. @@ -1492,50 +1253,3086 @@ func (mock *ObjectRefVirtualDiskSnapshotDiskServiceMock) GetCapacityCalls() []st return calls } -// Protect calls ProtectFunc. -func (mock *ObjectRefVirtualDiskSnapshotDiskServiceMock) Protect(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error { - if mock.ProtectFunc == nil { - panic("ObjectRefVirtualDiskSnapshotDiskServiceMock.ProtectFunc: method is nil but ObjectRefVirtualDiskSnapshotDiskService.Protect was just called") - } - callInfo := struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim - }{ - Ctx: ctx, - Sup: sup, - Owner: owner, - Dv: dv, - Pvc: pvc, - } - mock.lockProtect.Lock() - mock.calls.Protect = append(mock.calls.Protect, callInfo) - mock.lockProtect.Unlock() - return mock.ProtectFunc(ctx, sup, owner, dv, pvc) -} +// Ensure, that UploadDataSourceDiskServiceMock does implement UploadDataSourceDiskService. +// If this is not the case, regenerate this file with moq. +var _ UploadDataSourceDiskService = &UploadDataSourceDiskServiceMock{} -// ProtectCalls gets all the calls that were made to Protect. -// Check the length with: +// UploadDataSourceDiskServiceMock is a mock implementation of UploadDataSourceDiskService. // -// len(mockedObjectRefVirtualDiskSnapshotDiskService.ProtectCalls()) -func (mock *ObjectRefVirtualDiskSnapshotDiskServiceMock) ProtectCalls() []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim -} { - var calls []struct { - Ctx context.Context - Sup supplements.Generator - Owner client.Object - Dv *cdiv1.DataVolume - Pvc *corev1.PersistentVolumeClaim - } - mock.lockProtect.RLock() - calls = mock.calls.Protect - mock.lockProtect.RUnlock() +// func TestSomethingThatUsesUploadDataSourceDiskService(t *testing.T) { +// +// // make and configure a mocked UploadDataSourceDiskService +// mockedUploadDataSourceDiskService := &UploadDataSourceDiskServiceMock{ +// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// CleanUpSupplementsFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUpSupplements method") +// }, +// EnsurePVCImportFunc: func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { +// panic("mock out the EnsurePVCImport method") +// }, +// GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { +// panic("mock out the GetCapacity method") +// }, +// GetPersistentVolumeClaimFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { +// panic("mock out the GetPersistentVolumeClaim method") +// }, +// StartPVCImportFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { +// panic("mock out the StartPVCImport method") +// }, +// } +// +// // use mockedUploadDataSourceDiskService in code that requires UploadDataSourceDiskService +// // and then make assertions. +// +// } +type UploadDataSourceDiskServiceMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // CleanUpSupplementsFunc mocks the CleanUpSupplements method. + CleanUpSupplementsFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // EnsurePVCImportFunc mocks the EnsurePVCImport method. + EnsurePVCImportFunc func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) + + // GetCapacityFunc mocks the GetCapacity method. + GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string + + // GetPersistentVolumeClaimFunc mocks the GetPersistentVolumeClaim method. + GetPersistentVolumeClaimFunc func(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) + + // StartPVCImportFunc mocks the StartPVCImport method. + StartPVCImportFunc func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // CleanUpSupplements holds details about calls to the CleanUpSupplements method. + CleanUpSupplements []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // EnsurePVCImport holds details about calls to the EnsurePVCImport method. + EnsurePVCImport []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Target is the target argument value. + Target *corev1.PersistentVolumeClaim + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement + } + // GetCapacity holds details about calls to the GetCapacity method. + GetCapacity []struct { + // Pvc is the pvc argument value. + Pvc *corev1.PersistentVolumeClaim + } + // GetPersistentVolumeClaim holds details about calls to the GetPersistentVolumeClaim method. + GetPersistentVolumeClaim []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // StartPVCImport holds details about calls to the StartPVCImport method. + StartPVCImport []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // PvcSize is the pvcSize argument value. + PvcSize resource.Quantity + // Sc is the sc argument value. + Sc *v1.StorageClass + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement + } + } + lockCleanUp sync.RWMutex + lockCleanUpSupplements sync.RWMutex + lockEnsurePVCImport sync.RWMutex + lockGetCapacity sync.RWMutex + lockGetPersistentVolumeClaim sync.RWMutex + lockStartPVCImport sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *UploadDataSourceDiskServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("UploadDataSourceDiskServiceMock.CleanUpFunc: method is nil but UploadDataSourceDiskService.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedUploadDataSourceDiskService.CleanUpCalls()) +func (mock *UploadDataSourceDiskServiceMock) CleanUpCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// CleanUpSupplements calls CleanUpSupplementsFunc. +func (mock *UploadDataSourceDiskServiceMock) CleanUpSupplements(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpSupplementsFunc == nil { + panic("UploadDataSourceDiskServiceMock.CleanUpSupplementsFunc: method is nil but UploadDataSourceDiskService.CleanUpSupplements was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUpSupplements.Lock() + mock.calls.CleanUpSupplements = append(mock.calls.CleanUpSupplements, callInfo) + mock.lockCleanUpSupplements.Unlock() + return mock.CleanUpSupplementsFunc(ctx, sup) +} + +// CleanUpSupplementsCalls gets all the calls that were made to CleanUpSupplements. +// Check the length with: +// +// len(mockedUploadDataSourceDiskService.CleanUpSupplementsCalls()) +func (mock *UploadDataSourceDiskServiceMock) CleanUpSupplementsCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUpSupplements.RLock() + calls = mock.calls.CleanUpSupplements + mock.lockCleanUpSupplements.RUnlock() + return calls +} + +// EnsurePVCImport calls EnsurePVCImportFunc. +func (mock *UploadDataSourceDiskServiceMock) EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + if mock.EnsurePVCImportFunc == nil { + panic("UploadDataSourceDiskServiceMock.EnsurePVCImportFunc: method is nil but UploadDataSourceDiskService.EnsurePVCImport was just called") + } + callInfo := struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + Target: target, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockEnsurePVCImport.Lock() + mock.calls.EnsurePVCImport = append(mock.calls.EnsurePVCImport, callInfo) + mock.lockEnsurePVCImport.Unlock() + return mock.EnsurePVCImportFunc(ctx, target, source, vd, nodePlacement) +} + +// EnsurePVCImportCalls gets all the calls that were made to EnsurePVCImport. +// Check the length with: +// +// len(mockedUploadDataSourceDiskService.EnsurePVCImportCalls()) +func (mock *UploadDataSourceDiskServiceMock) EnsurePVCImportCalls() []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockEnsurePVCImport.RLock() + calls = mock.calls.EnsurePVCImport + mock.lockEnsurePVCImport.RUnlock() + return calls +} + +// GetCapacity calls GetCapacityFunc. +func (mock *UploadDataSourceDiskServiceMock) GetCapacity(pvc *corev1.PersistentVolumeClaim) string { + if mock.GetCapacityFunc == nil { + panic("UploadDataSourceDiskServiceMock.GetCapacityFunc: method is nil but UploadDataSourceDiskService.GetCapacity was just called") + } + callInfo := struct { + Pvc *corev1.PersistentVolumeClaim + }{ + Pvc: pvc, + } + mock.lockGetCapacity.Lock() + mock.calls.GetCapacity = append(mock.calls.GetCapacity, callInfo) + mock.lockGetCapacity.Unlock() + return mock.GetCapacityFunc(pvc) +} + +// GetCapacityCalls gets all the calls that were made to GetCapacity. +// Check the length with: +// +// len(mockedUploadDataSourceDiskService.GetCapacityCalls()) +func (mock *UploadDataSourceDiskServiceMock) GetCapacityCalls() []struct { + Pvc *corev1.PersistentVolumeClaim +} { + var calls []struct { + Pvc *corev1.PersistentVolumeClaim + } + mock.lockGetCapacity.RLock() + calls = mock.calls.GetCapacity + mock.lockGetCapacity.RUnlock() + return calls +} + +// GetPersistentVolumeClaim calls GetPersistentVolumeClaimFunc. +func (mock *UploadDataSourceDiskServiceMock) GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + if mock.GetPersistentVolumeClaimFunc == nil { + panic("UploadDataSourceDiskServiceMock.GetPersistentVolumeClaimFunc: method is nil but UploadDataSourceDiskService.GetPersistentVolumeClaim was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPersistentVolumeClaim.Lock() + mock.calls.GetPersistentVolumeClaim = append(mock.calls.GetPersistentVolumeClaim, callInfo) + mock.lockGetPersistentVolumeClaim.Unlock() + return mock.GetPersistentVolumeClaimFunc(ctx, sup) +} + +// GetPersistentVolumeClaimCalls gets all the calls that were made to GetPersistentVolumeClaim. +// Check the length with: +// +// len(mockedUploadDataSourceDiskService.GetPersistentVolumeClaimCalls()) +func (mock *UploadDataSourceDiskServiceMock) GetPersistentVolumeClaimCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetPersistentVolumeClaim.RLock() + calls = mock.calls.GetPersistentVolumeClaim + mock.lockGetPersistentVolumeClaim.RUnlock() + return calls +} + +// StartPVCImport calls StartPVCImportFunc. +func (mock *UploadDataSourceDiskServiceMock) StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + if mock.StartPVCImportFunc == nil { + panic("UploadDataSourceDiskServiceMock.StartPVCImportFunc: method is nil but UploadDataSourceDiskService.StartPVCImport was just called") + } + callInfo := struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + PvcSize: pvcSize, + Sc: sc, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockStartPVCImport.Lock() + mock.calls.StartPVCImport = append(mock.calls.StartPVCImport, callInfo) + mock.lockStartPVCImport.Unlock() + return mock.StartPVCImportFunc(ctx, pvcSize, sc, source, vd, nodePlacement) +} + +// StartPVCImportCalls gets all the calls that were made to StartPVCImport. +// Check the length with: +// +// len(mockedUploadDataSourceDiskService.StartPVCImportCalls()) +func (mock *UploadDataSourceDiskServiceMock) StartPVCImportCalls() []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockStartPVCImport.RLock() + calls = mock.calls.StartPVCImport + mock.lockStartPVCImport.RUnlock() + return calls +} + +// Ensure, that UploadDataSourceUploaderServiceMock does implement UploadDataSourceUploaderService. +// If this is not the case, regenerate this file with moq. +var _ UploadDataSourceUploaderService = &UploadDataSourceUploaderServiceMock{} + +// UploadDataSourceUploaderServiceMock is a mock implementation of UploadDataSourceUploaderService. +// +// func TestSomethingThatUsesUploadDataSourceUploaderService(t *testing.T) { +// +// // make and configure a mocked UploadDataSourceUploaderService +// mockedUploadDataSourceUploaderService := &UploadDataSourceUploaderServiceMock{ +// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// GetExternalURLFunc: func(ctx context.Context, ing *netv1.Ingress) string { +// panic("mock out the GetExternalURL method") +// }, +// GetInClusterURLFunc: func(ctx context.Context, svc *corev1.Service) string { +// panic("mock out the GetInClusterURL method") +// }, +// GetIngressFunc: func(ctx context.Context, sup supplements.Generator) (*netv1.Ingress, error) { +// panic("mock out the GetIngress method") +// }, +// GetPodFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) { +// panic("mock out the GetPod method") +// }, +// GetServiceFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.Service, error) { +// panic("mock out the GetService method") +// }, +// ProtectFunc: func(ctx context.Context, sup supplements.Generator, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { +// panic("mock out the Protect method") +// }, +// StartFunc: func(ctx context.Context, settings *uploader.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { +// panic("mock out the Start method") +// }, +// } +// +// // use mockedUploadDataSourceUploaderService in code that requires UploadDataSourceUploaderService +// // and then make assertions. +// +// } +type UploadDataSourceUploaderServiceMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // GetExternalURLFunc mocks the GetExternalURL method. + GetExternalURLFunc func(ctx context.Context, ing *netv1.Ingress) string + + // GetInClusterURLFunc mocks the GetInClusterURL method. + GetInClusterURLFunc func(ctx context.Context, svc *corev1.Service) string + + // GetIngressFunc mocks the GetIngress method. + GetIngressFunc func(ctx context.Context, sup supplements.Generator) (*netv1.Ingress, error) + + // GetPodFunc mocks the GetPod method. + GetPodFunc func(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) + + // GetServiceFunc mocks the GetService method. + GetServiceFunc func(ctx context.Context, sup supplements.Generator) (*corev1.Service, error) + + // ProtectFunc mocks the Protect method. + ProtectFunc func(ctx context.Context, sup supplements.Generator, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, settings *uploader.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // GetExternalURL holds details about calls to the GetExternalURL method. + GetExternalURL []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Ing is the ing argument value. + Ing *netv1.Ingress + } + // GetInClusterURL holds details about calls to the GetInClusterURL method. + GetInClusterURL []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Svc is the svc argument value. + Svc *corev1.Service + } + // GetIngress holds details about calls to the GetIngress method. + GetIngress []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // GetPod holds details about calls to the GetPod method. + GetPod []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // GetService holds details about calls to the GetService method. + GetService []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // Protect holds details about calls to the Protect method. + Protect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + // Pod is the pod argument value. + Pod *corev1.Pod + // Svc is the svc argument value. + Svc *corev1.Service + // Ing is the ing argument value. + Ing *netv1.Ingress + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Settings is the settings argument value. + Settings *uploader.Settings + // Obj is the obj argument value. + Obj client.Object + // Sup is the sup argument value. + Sup supplements.Generator + // CaBundle is the caBundle argument value. + CaBundle *datasource.CABundle + // Opts is the opts argument value. + Opts []service.Option + } + } + lockCleanUp sync.RWMutex + lockGetExternalURL sync.RWMutex + lockGetInClusterURL sync.RWMutex + lockGetIngress sync.RWMutex + lockGetPod sync.RWMutex + lockGetService sync.RWMutex + lockProtect sync.RWMutex + lockStart sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *UploadDataSourceUploaderServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("UploadDataSourceUploaderServiceMock.CleanUpFunc: method is nil but UploadDataSourceUploaderService.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.CleanUpCalls()) +func (mock *UploadDataSourceUploaderServiceMock) CleanUpCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// GetExternalURL calls GetExternalURLFunc. +func (mock *UploadDataSourceUploaderServiceMock) GetExternalURL(ctx context.Context, ing *netv1.Ingress) string { + if mock.GetExternalURLFunc == nil { + panic("UploadDataSourceUploaderServiceMock.GetExternalURLFunc: method is nil but UploadDataSourceUploaderService.GetExternalURL was just called") + } + callInfo := struct { + Ctx context.Context + Ing *netv1.Ingress + }{ + Ctx: ctx, + Ing: ing, + } + mock.lockGetExternalURL.Lock() + mock.calls.GetExternalURL = append(mock.calls.GetExternalURL, callInfo) + mock.lockGetExternalURL.Unlock() + return mock.GetExternalURLFunc(ctx, ing) +} + +// GetExternalURLCalls gets all the calls that were made to GetExternalURL. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.GetExternalURLCalls()) +func (mock *UploadDataSourceUploaderServiceMock) GetExternalURLCalls() []struct { + Ctx context.Context + Ing *netv1.Ingress +} { + var calls []struct { + Ctx context.Context + Ing *netv1.Ingress + } + mock.lockGetExternalURL.RLock() + calls = mock.calls.GetExternalURL + mock.lockGetExternalURL.RUnlock() + return calls +} + +// GetInClusterURL calls GetInClusterURLFunc. +func (mock *UploadDataSourceUploaderServiceMock) GetInClusterURL(ctx context.Context, svc *corev1.Service) string { + if mock.GetInClusterURLFunc == nil { + panic("UploadDataSourceUploaderServiceMock.GetInClusterURLFunc: method is nil but UploadDataSourceUploaderService.GetInClusterURL was just called") + } + callInfo := struct { + Ctx context.Context + Svc *corev1.Service + }{ + Ctx: ctx, + Svc: svc, + } + mock.lockGetInClusterURL.Lock() + mock.calls.GetInClusterURL = append(mock.calls.GetInClusterURL, callInfo) + mock.lockGetInClusterURL.Unlock() + return mock.GetInClusterURLFunc(ctx, svc) +} + +// GetInClusterURLCalls gets all the calls that were made to GetInClusterURL. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.GetInClusterURLCalls()) +func (mock *UploadDataSourceUploaderServiceMock) GetInClusterURLCalls() []struct { + Ctx context.Context + Svc *corev1.Service +} { + var calls []struct { + Ctx context.Context + Svc *corev1.Service + } + mock.lockGetInClusterURL.RLock() + calls = mock.calls.GetInClusterURL + mock.lockGetInClusterURL.RUnlock() + return calls +} + +// GetIngress calls GetIngressFunc. +func (mock *UploadDataSourceUploaderServiceMock) GetIngress(ctx context.Context, sup supplements.Generator) (*netv1.Ingress, error) { + if mock.GetIngressFunc == nil { + panic("UploadDataSourceUploaderServiceMock.GetIngressFunc: method is nil but UploadDataSourceUploaderService.GetIngress was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetIngress.Lock() + mock.calls.GetIngress = append(mock.calls.GetIngress, callInfo) + mock.lockGetIngress.Unlock() + return mock.GetIngressFunc(ctx, sup) +} + +// GetIngressCalls gets all the calls that were made to GetIngress. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.GetIngressCalls()) +func (mock *UploadDataSourceUploaderServiceMock) GetIngressCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetIngress.RLock() + calls = mock.calls.GetIngress + mock.lockGetIngress.RUnlock() + return calls +} + +// GetPod calls GetPodFunc. +func (mock *UploadDataSourceUploaderServiceMock) GetPod(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) { + if mock.GetPodFunc == nil { + panic("UploadDataSourceUploaderServiceMock.GetPodFunc: method is nil but UploadDataSourceUploaderService.GetPod was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPod.Lock() + mock.calls.GetPod = append(mock.calls.GetPod, callInfo) + mock.lockGetPod.Unlock() + return mock.GetPodFunc(ctx, sup) +} + +// GetPodCalls gets all the calls that were made to GetPod. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.GetPodCalls()) +func (mock *UploadDataSourceUploaderServiceMock) GetPodCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetPod.RLock() + calls = mock.calls.GetPod + mock.lockGetPod.RUnlock() + return calls +} + +// GetService calls GetServiceFunc. +func (mock *UploadDataSourceUploaderServiceMock) GetService(ctx context.Context, sup supplements.Generator) (*corev1.Service, error) { + if mock.GetServiceFunc == nil { + panic("UploadDataSourceUploaderServiceMock.GetServiceFunc: method is nil but UploadDataSourceUploaderService.GetService was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetService.Lock() + mock.calls.GetService = append(mock.calls.GetService, callInfo) + mock.lockGetService.Unlock() + return mock.GetServiceFunc(ctx, sup) +} + +// GetServiceCalls gets all the calls that were made to GetService. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.GetServiceCalls()) +func (mock *UploadDataSourceUploaderServiceMock) GetServiceCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetService.RLock() + calls = mock.calls.GetService + mock.lockGetService.RUnlock() + return calls +} + +// Protect calls ProtectFunc. +func (mock *UploadDataSourceUploaderServiceMock) Protect(ctx context.Context, sup supplements.Generator, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { + if mock.ProtectFunc == nil { + panic("UploadDataSourceUploaderServiceMock.ProtectFunc: method is nil but UploadDataSourceUploaderService.Protect was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + }{ + Ctx: ctx, + Sup: sup, + Pod: pod, + Svc: svc, + Ing: ing, + } + mock.lockProtect.Lock() + mock.calls.Protect = append(mock.calls.Protect, callInfo) + mock.lockProtect.Unlock() + return mock.ProtectFunc(ctx, sup, pod, svc, ing) +} + +// ProtectCalls gets all the calls that were made to Protect. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.ProtectCalls()) +func (mock *UploadDataSourceUploaderServiceMock) ProtectCalls() []struct { + Ctx context.Context + Sup supplements.Generator + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + } + mock.lockProtect.RLock() + calls = mock.calls.Protect + mock.lockProtect.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *UploadDataSourceUploaderServiceMock) Start(ctx context.Context, settings *uploader.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { + if mock.StartFunc == nil { + panic("UploadDataSourceUploaderServiceMock.StartFunc: method is nil but UploadDataSourceUploaderService.Start was just called") + } + callInfo := struct { + Ctx context.Context + Settings *uploader.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option + }{ + Ctx: ctx, + Settings: settings, + Obj: obj, + Sup: sup, + CaBundle: caBundle, + Opts: opts, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + return mock.StartFunc(ctx, settings, obj, sup, caBundle, opts...) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// +// len(mockedUploadDataSourceUploaderService.StartCalls()) +func (mock *UploadDataSourceUploaderServiceMock) StartCalls() []struct { + Ctx context.Context + Settings *uploader.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option +} { + var calls []struct { + Ctx context.Context + Settings *uploader.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} + +// Ensure, that UploadDataSourceStatServiceMock does implement UploadDataSourceStatService. +// If this is not the case, regenerate this file with moq. +var _ UploadDataSourceStatService = &UploadDataSourceStatServiceMock{} + +// UploadDataSourceStatServiceMock is a mock implementation of UploadDataSourceStatService. +// +// func TestSomethingThatUsesUploadDataSourceStatService(t *testing.T) { +// +// // make and configure a mocked UploadDataSourceStatService +// mockedUploadDataSourceStatService := &UploadDataSourceStatServiceMock{ +// CheckPodFunc: func(pod *corev1.Pod) error { +// panic("mock out the CheckPod method") +// }, +// GetDVCRImageNameFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetDVCRImageName method") +// }, +// GetDownloadSpeedFunc: func(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed { +// panic("mock out the GetDownloadSpeed method") +// }, +// GetFormatFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetFormat method") +// }, +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// GetSizeFunc: func(pod *corev1.Pod) v1alpha2.ImageStatusSize { +// panic("mock out the GetSize method") +// }, +// IsUploadStartedFunc: func(ownerUID types.UID, pod *corev1.Pod) bool { +// panic("mock out the IsUploadStarted method") +// }, +// IsUploaderReadyFunc: func(pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress, tlsSecret *corev1.Secret) (bool, error) { +// panic("mock out the IsUploaderReady method") +// }, +// } +// +// // use mockedUploadDataSourceStatService in code that requires UploadDataSourceStatService +// // and then make assertions. +// +// } +type UploadDataSourceStatServiceMock struct { + // CheckPodFunc mocks the CheckPod method. + CheckPodFunc func(pod *corev1.Pod) error + + // GetDVCRImageNameFunc mocks the GetDVCRImageName method. + GetDVCRImageNameFunc func(pod *corev1.Pod) string + + // GetDownloadSpeedFunc mocks the GetDownloadSpeed method. + GetDownloadSpeedFunc func(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed + + // GetFormatFunc mocks the GetFormat method. + GetFormatFunc func(pod *corev1.Pod) string + + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + + // GetSizeFunc mocks the GetSize method. + GetSizeFunc func(pod *corev1.Pod) v1alpha2.ImageStatusSize + + // IsUploadStartedFunc mocks the IsUploadStarted method. + IsUploadStartedFunc func(ownerUID types.UID, pod *corev1.Pod) bool + + // IsUploaderReadyFunc mocks the IsUploaderReady method. + IsUploaderReadyFunc func(pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress, tlsSecret *corev1.Secret) (bool, error) + + // calls tracks calls to the methods. + calls struct { + // CheckPod holds details about calls to the CheckPod method. + CheckPod []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDVCRImageName holds details about calls to the GetDVCRImageName method. + GetDVCRImageName []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDownloadSpeed holds details about calls to the GetDownloadSpeed method. + GetDownloadSpeed []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetFormat holds details about calls to the GetFormat method. + GetFormat []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } + // GetSize holds details about calls to the GetSize method. + GetSize []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // IsUploadStarted holds details about calls to the IsUploadStarted method. + IsUploadStarted []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + } + // IsUploaderReady holds details about calls to the IsUploaderReady method. + IsUploaderReady []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + // Svc is the svc argument value. + Svc *corev1.Service + // Ing is the ing argument value. + Ing *netv1.Ingress + // TlsSecret is the tlsSecret argument value. + TlsSecret *corev1.Secret + } + } + lockCheckPod sync.RWMutex + lockGetDVCRImageName sync.RWMutex + lockGetDownloadSpeed sync.RWMutex + lockGetFormat sync.RWMutex + lockGetProgress sync.RWMutex + lockGetSize sync.RWMutex + lockIsUploadStarted sync.RWMutex + lockIsUploaderReady sync.RWMutex +} + +// CheckPod calls CheckPodFunc. +func (mock *UploadDataSourceStatServiceMock) CheckPod(pod *corev1.Pod) error { + if mock.CheckPodFunc == nil { + panic("UploadDataSourceStatServiceMock.CheckPodFunc: method is nil but UploadDataSourceStatService.CheckPod was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockCheckPod.Lock() + mock.calls.CheckPod = append(mock.calls.CheckPod, callInfo) + mock.lockCheckPod.Unlock() + return mock.CheckPodFunc(pod) +} + +// CheckPodCalls gets all the calls that were made to CheckPod. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.CheckPodCalls()) +func (mock *UploadDataSourceStatServiceMock) CheckPodCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockCheckPod.RLock() + calls = mock.calls.CheckPod + mock.lockCheckPod.RUnlock() + return calls +} + +// GetDVCRImageName calls GetDVCRImageNameFunc. +func (mock *UploadDataSourceStatServiceMock) GetDVCRImageName(pod *corev1.Pod) string { + if mock.GetDVCRImageNameFunc == nil { + panic("UploadDataSourceStatServiceMock.GetDVCRImageNameFunc: method is nil but UploadDataSourceStatService.GetDVCRImageName was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetDVCRImageName.Lock() + mock.calls.GetDVCRImageName = append(mock.calls.GetDVCRImageName, callInfo) + mock.lockGetDVCRImageName.Unlock() + return mock.GetDVCRImageNameFunc(pod) +} + +// GetDVCRImageNameCalls gets all the calls that were made to GetDVCRImageName. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.GetDVCRImageNameCalls()) +func (mock *UploadDataSourceStatServiceMock) GetDVCRImageNameCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetDVCRImageName.RLock() + calls = mock.calls.GetDVCRImageName + mock.lockGetDVCRImageName.RUnlock() + return calls +} + +// GetDownloadSpeed calls GetDownloadSpeedFunc. +func (mock *UploadDataSourceStatServiceMock) GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed { + if mock.GetDownloadSpeedFunc == nil { + panic("UploadDataSourceStatServiceMock.GetDownloadSpeedFunc: method is nil but UploadDataSourceStatService.GetDownloadSpeed was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + }{ + OwnerUID: ownerUID, + Pod: pod, + } + mock.lockGetDownloadSpeed.Lock() + mock.calls.GetDownloadSpeed = append(mock.calls.GetDownloadSpeed, callInfo) + mock.lockGetDownloadSpeed.Unlock() + return mock.GetDownloadSpeedFunc(ownerUID, pod) +} + +// GetDownloadSpeedCalls gets all the calls that were made to GetDownloadSpeed. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.GetDownloadSpeedCalls()) +func (mock *UploadDataSourceStatServiceMock) GetDownloadSpeedCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + } + mock.lockGetDownloadSpeed.RLock() + calls = mock.calls.GetDownloadSpeed + mock.lockGetDownloadSpeed.RUnlock() + return calls +} + +// GetFormat calls GetFormatFunc. +func (mock *UploadDataSourceStatServiceMock) GetFormat(pod *corev1.Pod) string { + if mock.GetFormatFunc == nil { + panic("UploadDataSourceStatServiceMock.GetFormatFunc: method is nil but UploadDataSourceStatService.GetFormat was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetFormat.Lock() + mock.calls.GetFormat = append(mock.calls.GetFormat, callInfo) + mock.lockGetFormat.Unlock() + return mock.GetFormatFunc(pod) +} + +// GetFormatCalls gets all the calls that were made to GetFormat. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.GetFormatCalls()) +func (mock *UploadDataSourceStatServiceMock) GetFormatCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetFormat.RLock() + calls = mock.calls.GetFormat + mock.lockGetFormat.RUnlock() + return calls +} + +// GetProgress calls GetProgressFunc. +func (mock *UploadDataSourceStatServiceMock) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("UploadDataSourceStatServiceMock.GetProgressFunc: method is nil but UploadDataSourceStatService.GetProgress was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + }{ + OwnerUID: ownerUID, + Pod: pod, + PrevProgress: prevProgress, + Opts: opts, + } + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(ownerUID, pod, prevProgress, opts...) +} + +// GetProgressCalls gets all the calls that were made to GetProgress. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.GetProgressCalls()) +func (mock *UploadDataSourceStatServiceMock) GetProgressCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + } + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() + return calls +} + +// GetSize calls GetSizeFunc. +func (mock *UploadDataSourceStatServiceMock) GetSize(pod *corev1.Pod) v1alpha2.ImageStatusSize { + if mock.GetSizeFunc == nil { + panic("UploadDataSourceStatServiceMock.GetSizeFunc: method is nil but UploadDataSourceStatService.GetSize was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetSize.Lock() + mock.calls.GetSize = append(mock.calls.GetSize, callInfo) + mock.lockGetSize.Unlock() + return mock.GetSizeFunc(pod) +} + +// GetSizeCalls gets all the calls that were made to GetSize. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.GetSizeCalls()) +func (mock *UploadDataSourceStatServiceMock) GetSizeCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetSize.RLock() + calls = mock.calls.GetSize + mock.lockGetSize.RUnlock() + return calls +} + +// IsUploadStarted calls IsUploadStartedFunc. +func (mock *UploadDataSourceStatServiceMock) IsUploadStarted(ownerUID types.UID, pod *corev1.Pod) bool { + if mock.IsUploadStartedFunc == nil { + panic("UploadDataSourceStatServiceMock.IsUploadStartedFunc: method is nil but UploadDataSourceStatService.IsUploadStarted was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + }{ + OwnerUID: ownerUID, + Pod: pod, + } + mock.lockIsUploadStarted.Lock() + mock.calls.IsUploadStarted = append(mock.calls.IsUploadStarted, callInfo) + mock.lockIsUploadStarted.Unlock() + return mock.IsUploadStartedFunc(ownerUID, pod) +} + +// IsUploadStartedCalls gets all the calls that were made to IsUploadStarted. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.IsUploadStartedCalls()) +func (mock *UploadDataSourceStatServiceMock) IsUploadStartedCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + } + mock.lockIsUploadStarted.RLock() + calls = mock.calls.IsUploadStarted + mock.lockIsUploadStarted.RUnlock() + return calls +} + +// IsUploaderReady calls IsUploaderReadyFunc. +func (mock *UploadDataSourceStatServiceMock) IsUploaderReady(pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress, tlsSecret *corev1.Secret) (bool, error) { + if mock.IsUploaderReadyFunc == nil { + panic("UploadDataSourceStatServiceMock.IsUploaderReadyFunc: method is nil but UploadDataSourceStatService.IsUploaderReady was just called") + } + callInfo := struct { + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + TlsSecret *corev1.Secret + }{ + Pod: pod, + Svc: svc, + Ing: ing, + TlsSecret: tlsSecret, + } + mock.lockIsUploaderReady.Lock() + mock.calls.IsUploaderReady = append(mock.calls.IsUploaderReady, callInfo) + mock.lockIsUploaderReady.Unlock() + return mock.IsUploaderReadyFunc(pod, svc, ing, tlsSecret) +} + +// IsUploaderReadyCalls gets all the calls that were made to IsUploaderReady. +// Check the length with: +// +// len(mockedUploadDataSourceStatService.IsUploaderReadyCalls()) +func (mock *UploadDataSourceStatServiceMock) IsUploaderReadyCalls() []struct { + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + TlsSecret *corev1.Secret +} { + var calls []struct { + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + TlsSecret *corev1.Secret + } + mock.lockIsUploaderReady.RLock() + calls = mock.calls.IsUploaderReady + mock.lockIsUploaderReady.RUnlock() + return calls +} + +// Ensure, that HTTPDataSourceDiskServiceMock does implement HTTPDataSourceDiskService. +// If this is not the case, regenerate this file with moq. +var _ HTTPDataSourceDiskService = &HTTPDataSourceDiskServiceMock{} + +// HTTPDataSourceDiskServiceMock is a mock implementation of HTTPDataSourceDiskService. +// +// func TestSomethingThatUsesHTTPDataSourceDiskService(t *testing.T) { +// +// // make and configure a mocked HTTPDataSourceDiskService +// mockedHTTPDataSourceDiskService := &HTTPDataSourceDiskServiceMock{ +// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// CleanUpSupplementsFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUpSupplements method") +// }, +// EnsurePVCImportFunc: func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { +// panic("mock out the EnsurePVCImport method") +// }, +// GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { +// panic("mock out the GetCapacity method") +// }, +// GetPersistentVolumeClaimFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { +// panic("mock out the GetPersistentVolumeClaim method") +// }, +// StartPVCImportFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { +// panic("mock out the StartPVCImport method") +// }, +// } +// +// // use mockedHTTPDataSourceDiskService in code that requires HTTPDataSourceDiskService +// // and then make assertions. +// +// } +type HTTPDataSourceDiskServiceMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // CleanUpSupplementsFunc mocks the CleanUpSupplements method. + CleanUpSupplementsFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // EnsurePVCImportFunc mocks the EnsurePVCImport method. + EnsurePVCImportFunc func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) + + // GetCapacityFunc mocks the GetCapacity method. + GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string + + // GetPersistentVolumeClaimFunc mocks the GetPersistentVolumeClaim method. + GetPersistentVolumeClaimFunc func(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) + + // StartPVCImportFunc mocks the StartPVCImport method. + StartPVCImportFunc func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // CleanUpSupplements holds details about calls to the CleanUpSupplements method. + CleanUpSupplements []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // EnsurePVCImport holds details about calls to the EnsurePVCImport method. + EnsurePVCImport []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Target is the target argument value. + Target *corev1.PersistentVolumeClaim + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement + } + // GetCapacity holds details about calls to the GetCapacity method. + GetCapacity []struct { + // Pvc is the pvc argument value. + Pvc *corev1.PersistentVolumeClaim + } + // GetPersistentVolumeClaim holds details about calls to the GetPersistentVolumeClaim method. + GetPersistentVolumeClaim []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // StartPVCImport holds details about calls to the StartPVCImport method. + StartPVCImport []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // PvcSize is the pvcSize argument value. + PvcSize resource.Quantity + // Sc is the sc argument value. + Sc *v1.StorageClass + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement + } + } + lockCleanUp sync.RWMutex + lockCleanUpSupplements sync.RWMutex + lockEnsurePVCImport sync.RWMutex + lockGetCapacity sync.RWMutex + lockGetPersistentVolumeClaim sync.RWMutex + lockStartPVCImport sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *HTTPDataSourceDiskServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("HTTPDataSourceDiskServiceMock.CleanUpFunc: method is nil but HTTPDataSourceDiskService.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedHTTPDataSourceDiskService.CleanUpCalls()) +func (mock *HTTPDataSourceDiskServiceMock) CleanUpCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// CleanUpSupplements calls CleanUpSupplementsFunc. +func (mock *HTTPDataSourceDiskServiceMock) CleanUpSupplements(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpSupplementsFunc == nil { + panic("HTTPDataSourceDiskServiceMock.CleanUpSupplementsFunc: method is nil but HTTPDataSourceDiskService.CleanUpSupplements was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUpSupplements.Lock() + mock.calls.CleanUpSupplements = append(mock.calls.CleanUpSupplements, callInfo) + mock.lockCleanUpSupplements.Unlock() + return mock.CleanUpSupplementsFunc(ctx, sup) +} + +// CleanUpSupplementsCalls gets all the calls that were made to CleanUpSupplements. +// Check the length with: +// +// len(mockedHTTPDataSourceDiskService.CleanUpSupplementsCalls()) +func (mock *HTTPDataSourceDiskServiceMock) CleanUpSupplementsCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUpSupplements.RLock() + calls = mock.calls.CleanUpSupplements + mock.lockCleanUpSupplements.RUnlock() + return calls +} + +// EnsurePVCImport calls EnsurePVCImportFunc. +func (mock *HTTPDataSourceDiskServiceMock) EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + if mock.EnsurePVCImportFunc == nil { + panic("HTTPDataSourceDiskServiceMock.EnsurePVCImportFunc: method is nil but HTTPDataSourceDiskService.EnsurePVCImport was just called") + } + callInfo := struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + Target: target, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockEnsurePVCImport.Lock() + mock.calls.EnsurePVCImport = append(mock.calls.EnsurePVCImport, callInfo) + mock.lockEnsurePVCImport.Unlock() + return mock.EnsurePVCImportFunc(ctx, target, source, vd, nodePlacement) +} + +// EnsurePVCImportCalls gets all the calls that were made to EnsurePVCImport. +// Check the length with: +// +// len(mockedHTTPDataSourceDiskService.EnsurePVCImportCalls()) +func (mock *HTTPDataSourceDiskServiceMock) EnsurePVCImportCalls() []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockEnsurePVCImport.RLock() + calls = mock.calls.EnsurePVCImport + mock.lockEnsurePVCImport.RUnlock() + return calls +} + +// GetCapacity calls GetCapacityFunc. +func (mock *HTTPDataSourceDiskServiceMock) GetCapacity(pvc *corev1.PersistentVolumeClaim) string { + if mock.GetCapacityFunc == nil { + panic("HTTPDataSourceDiskServiceMock.GetCapacityFunc: method is nil but HTTPDataSourceDiskService.GetCapacity was just called") + } + callInfo := struct { + Pvc *corev1.PersistentVolumeClaim + }{ + Pvc: pvc, + } + mock.lockGetCapacity.Lock() + mock.calls.GetCapacity = append(mock.calls.GetCapacity, callInfo) + mock.lockGetCapacity.Unlock() + return mock.GetCapacityFunc(pvc) +} + +// GetCapacityCalls gets all the calls that were made to GetCapacity. +// Check the length with: +// +// len(mockedHTTPDataSourceDiskService.GetCapacityCalls()) +func (mock *HTTPDataSourceDiskServiceMock) GetCapacityCalls() []struct { + Pvc *corev1.PersistentVolumeClaim +} { + var calls []struct { + Pvc *corev1.PersistentVolumeClaim + } + mock.lockGetCapacity.RLock() + calls = mock.calls.GetCapacity + mock.lockGetCapacity.RUnlock() + return calls +} + +// GetPersistentVolumeClaim calls GetPersistentVolumeClaimFunc. +func (mock *HTTPDataSourceDiskServiceMock) GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + if mock.GetPersistentVolumeClaimFunc == nil { + panic("HTTPDataSourceDiskServiceMock.GetPersistentVolumeClaimFunc: method is nil but HTTPDataSourceDiskService.GetPersistentVolumeClaim was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPersistentVolumeClaim.Lock() + mock.calls.GetPersistentVolumeClaim = append(mock.calls.GetPersistentVolumeClaim, callInfo) + mock.lockGetPersistentVolumeClaim.Unlock() + return mock.GetPersistentVolumeClaimFunc(ctx, sup) +} + +// GetPersistentVolumeClaimCalls gets all the calls that were made to GetPersistentVolumeClaim. +// Check the length with: +// +// len(mockedHTTPDataSourceDiskService.GetPersistentVolumeClaimCalls()) +func (mock *HTTPDataSourceDiskServiceMock) GetPersistentVolumeClaimCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetPersistentVolumeClaim.RLock() + calls = mock.calls.GetPersistentVolumeClaim + mock.lockGetPersistentVolumeClaim.RUnlock() + return calls +} + +// StartPVCImport calls StartPVCImportFunc. +func (mock *HTTPDataSourceDiskServiceMock) StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + if mock.StartPVCImportFunc == nil { + panic("HTTPDataSourceDiskServiceMock.StartPVCImportFunc: method is nil but HTTPDataSourceDiskService.StartPVCImport was just called") + } + callInfo := struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + PvcSize: pvcSize, + Sc: sc, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockStartPVCImport.Lock() + mock.calls.StartPVCImport = append(mock.calls.StartPVCImport, callInfo) + mock.lockStartPVCImport.Unlock() + return mock.StartPVCImportFunc(ctx, pvcSize, sc, source, vd, nodePlacement) +} + +// StartPVCImportCalls gets all the calls that were made to StartPVCImport. +// Check the length with: +// +// len(mockedHTTPDataSourceDiskService.StartPVCImportCalls()) +func (mock *HTTPDataSourceDiskServiceMock) StartPVCImportCalls() []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockStartPVCImport.RLock() + calls = mock.calls.StartPVCImport + mock.lockStartPVCImport.RUnlock() + return calls +} + +// Ensure, that HTTPDataSourceImporterServiceMock does implement HTTPDataSourceImporterService. +// If this is not the case, regenerate this file with moq. +var _ HTTPDataSourceImporterService = &HTTPDataSourceImporterServiceMock{} + +// HTTPDataSourceImporterServiceMock is a mock implementation of HTTPDataSourceImporterService. +// +// func TestSomethingThatUsesHTTPDataSourceImporterService(t *testing.T) { +// +// // make and configure a mocked HTTPDataSourceImporterService +// mockedHTTPDataSourceImporterService := &HTTPDataSourceImporterServiceMock{ +// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// GetPodFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) { +// panic("mock out the GetPod method") +// }, +// ProtectFunc: func(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error { +// panic("mock out the Protect method") +// }, +// StartFunc: func(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { +// panic("mock out the Start method") +// }, +// } +// +// // use mockedHTTPDataSourceImporterService in code that requires HTTPDataSourceImporterService +// // and then make assertions. +// +// } +type HTTPDataSourceImporterServiceMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // GetPodFunc mocks the GetPod method. + GetPodFunc func(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) + + // ProtectFunc mocks the Protect method. + ProtectFunc func(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // GetPod holds details about calls to the GetPod method. + GetPod []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // Protect holds details about calls to the Protect method. + Protect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Pod is the pod argument value. + Pod *corev1.Pod + // Sup is the sup argument value. + Sup supplements.Generator + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Settings is the settings argument value. + Settings *importer.Settings + // Obj is the obj argument value. + Obj client.Object + // Sup is the sup argument value. + Sup supplements.Generator + // CaBundle is the caBundle argument value. + CaBundle *datasource.CABundle + // Opts is the opts argument value. + Opts []service.Option + } + } + lockCleanUp sync.RWMutex + lockGetPod sync.RWMutex + lockProtect sync.RWMutex + lockStart sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *HTTPDataSourceImporterServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("HTTPDataSourceImporterServiceMock.CleanUpFunc: method is nil but HTTPDataSourceImporterService.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedHTTPDataSourceImporterService.CleanUpCalls()) +func (mock *HTTPDataSourceImporterServiceMock) CleanUpCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// GetPod calls GetPodFunc. +func (mock *HTTPDataSourceImporterServiceMock) GetPod(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) { + if mock.GetPodFunc == nil { + panic("HTTPDataSourceImporterServiceMock.GetPodFunc: method is nil but HTTPDataSourceImporterService.GetPod was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPod.Lock() + mock.calls.GetPod = append(mock.calls.GetPod, callInfo) + mock.lockGetPod.Unlock() + return mock.GetPodFunc(ctx, sup) +} + +// GetPodCalls gets all the calls that were made to GetPod. +// Check the length with: +// +// len(mockedHTTPDataSourceImporterService.GetPodCalls()) +func (mock *HTTPDataSourceImporterServiceMock) GetPodCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetPod.RLock() + calls = mock.calls.GetPod + mock.lockGetPod.RUnlock() + return calls +} + +// Protect calls ProtectFunc. +func (mock *HTTPDataSourceImporterServiceMock) Protect(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error { + if mock.ProtectFunc == nil { + panic("HTTPDataSourceImporterServiceMock.ProtectFunc: method is nil but HTTPDataSourceImporterService.Protect was just called") + } + callInfo := struct { + Ctx context.Context + Pod *corev1.Pod + Sup supplements.Generator + }{ + Ctx: ctx, + Pod: pod, + Sup: sup, + } + mock.lockProtect.Lock() + mock.calls.Protect = append(mock.calls.Protect, callInfo) + mock.lockProtect.Unlock() + return mock.ProtectFunc(ctx, pod, sup) +} + +// ProtectCalls gets all the calls that were made to Protect. +// Check the length with: +// +// len(mockedHTTPDataSourceImporterService.ProtectCalls()) +func (mock *HTTPDataSourceImporterServiceMock) ProtectCalls() []struct { + Ctx context.Context + Pod *corev1.Pod + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Pod *corev1.Pod + Sup supplements.Generator + } + mock.lockProtect.RLock() + calls = mock.calls.Protect + mock.lockProtect.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *HTTPDataSourceImporterServiceMock) Start(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { + if mock.StartFunc == nil { + panic("HTTPDataSourceImporterServiceMock.StartFunc: method is nil but HTTPDataSourceImporterService.Start was just called") + } + callInfo := struct { + Ctx context.Context + Settings *importer.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option + }{ + Ctx: ctx, + Settings: settings, + Obj: obj, + Sup: sup, + CaBundle: caBundle, + Opts: opts, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + return mock.StartFunc(ctx, settings, obj, sup, caBundle, opts...) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// +// len(mockedHTTPDataSourceImporterService.StartCalls()) +func (mock *HTTPDataSourceImporterServiceMock) StartCalls() []struct { + Ctx context.Context + Settings *importer.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option +} { + var calls []struct { + Ctx context.Context + Settings *importer.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} + +// Ensure, that HTTPDataSourceStatServiceMock does implement HTTPDataSourceStatService. +// If this is not the case, regenerate this file with moq. +var _ HTTPDataSourceStatService = &HTTPDataSourceStatServiceMock{} + +// HTTPDataSourceStatServiceMock is a mock implementation of HTTPDataSourceStatService. +// +// func TestSomethingThatUsesHTTPDataSourceStatService(t *testing.T) { +// +// // make and configure a mocked HTTPDataSourceStatService +// mockedHTTPDataSourceStatService := &HTTPDataSourceStatServiceMock{ +// CheckPodFunc: func(pod *corev1.Pod) error { +// panic("mock out the CheckPod method") +// }, +// GetDVCRImageNameFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetDVCRImageName method") +// }, +// GetDownloadSpeedFunc: func(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed { +// panic("mock out the GetDownloadSpeed method") +// }, +// GetFormatFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetFormat method") +// }, +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// GetSizeFunc: func(pod *corev1.Pod) v1alpha2.ImageStatusSize { +// panic("mock out the GetSize method") +// }, +// } +// +// // use mockedHTTPDataSourceStatService in code that requires HTTPDataSourceStatService +// // and then make assertions. +// +// } +type HTTPDataSourceStatServiceMock struct { + // CheckPodFunc mocks the CheckPod method. + CheckPodFunc func(pod *corev1.Pod) error + + // GetDVCRImageNameFunc mocks the GetDVCRImageName method. + GetDVCRImageNameFunc func(pod *corev1.Pod) string + + // GetDownloadSpeedFunc mocks the GetDownloadSpeed method. + GetDownloadSpeedFunc func(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed + + // GetFormatFunc mocks the GetFormat method. + GetFormatFunc func(pod *corev1.Pod) string + + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + + // GetSizeFunc mocks the GetSize method. + GetSizeFunc func(pod *corev1.Pod) v1alpha2.ImageStatusSize + + // calls tracks calls to the methods. + calls struct { + // CheckPod holds details about calls to the CheckPod method. + CheckPod []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDVCRImageName holds details about calls to the GetDVCRImageName method. + GetDVCRImageName []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDownloadSpeed holds details about calls to the GetDownloadSpeed method. + GetDownloadSpeed []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetFormat holds details about calls to the GetFormat method. + GetFormat []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } + // GetSize holds details about calls to the GetSize method. + GetSize []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + } + lockCheckPod sync.RWMutex + lockGetDVCRImageName sync.RWMutex + lockGetDownloadSpeed sync.RWMutex + lockGetFormat sync.RWMutex + lockGetProgress sync.RWMutex + lockGetSize sync.RWMutex +} + +// CheckPod calls CheckPodFunc. +func (mock *HTTPDataSourceStatServiceMock) CheckPod(pod *corev1.Pod) error { + if mock.CheckPodFunc == nil { + panic("HTTPDataSourceStatServiceMock.CheckPodFunc: method is nil but HTTPDataSourceStatService.CheckPod was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockCheckPod.Lock() + mock.calls.CheckPod = append(mock.calls.CheckPod, callInfo) + mock.lockCheckPod.Unlock() + return mock.CheckPodFunc(pod) +} + +// CheckPodCalls gets all the calls that were made to CheckPod. +// Check the length with: +// +// len(mockedHTTPDataSourceStatService.CheckPodCalls()) +func (mock *HTTPDataSourceStatServiceMock) CheckPodCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockCheckPod.RLock() + calls = mock.calls.CheckPod + mock.lockCheckPod.RUnlock() + return calls +} + +// GetDVCRImageName calls GetDVCRImageNameFunc. +func (mock *HTTPDataSourceStatServiceMock) GetDVCRImageName(pod *corev1.Pod) string { + if mock.GetDVCRImageNameFunc == nil { + panic("HTTPDataSourceStatServiceMock.GetDVCRImageNameFunc: method is nil but HTTPDataSourceStatService.GetDVCRImageName was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetDVCRImageName.Lock() + mock.calls.GetDVCRImageName = append(mock.calls.GetDVCRImageName, callInfo) + mock.lockGetDVCRImageName.Unlock() + return mock.GetDVCRImageNameFunc(pod) +} + +// GetDVCRImageNameCalls gets all the calls that were made to GetDVCRImageName. +// Check the length with: +// +// len(mockedHTTPDataSourceStatService.GetDVCRImageNameCalls()) +func (mock *HTTPDataSourceStatServiceMock) GetDVCRImageNameCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetDVCRImageName.RLock() + calls = mock.calls.GetDVCRImageName + mock.lockGetDVCRImageName.RUnlock() + return calls +} + +// GetDownloadSpeed calls GetDownloadSpeedFunc. +func (mock *HTTPDataSourceStatServiceMock) GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed { + if mock.GetDownloadSpeedFunc == nil { + panic("HTTPDataSourceStatServiceMock.GetDownloadSpeedFunc: method is nil but HTTPDataSourceStatService.GetDownloadSpeed was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + }{ + OwnerUID: ownerUID, + Pod: pod, + } + mock.lockGetDownloadSpeed.Lock() + mock.calls.GetDownloadSpeed = append(mock.calls.GetDownloadSpeed, callInfo) + mock.lockGetDownloadSpeed.Unlock() + return mock.GetDownloadSpeedFunc(ownerUID, pod) +} + +// GetDownloadSpeedCalls gets all the calls that were made to GetDownloadSpeed. +// Check the length with: +// +// len(mockedHTTPDataSourceStatService.GetDownloadSpeedCalls()) +func (mock *HTTPDataSourceStatServiceMock) GetDownloadSpeedCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + } + mock.lockGetDownloadSpeed.RLock() + calls = mock.calls.GetDownloadSpeed + mock.lockGetDownloadSpeed.RUnlock() + return calls +} + +// GetFormat calls GetFormatFunc. +func (mock *HTTPDataSourceStatServiceMock) GetFormat(pod *corev1.Pod) string { + if mock.GetFormatFunc == nil { + panic("HTTPDataSourceStatServiceMock.GetFormatFunc: method is nil but HTTPDataSourceStatService.GetFormat was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetFormat.Lock() + mock.calls.GetFormat = append(mock.calls.GetFormat, callInfo) + mock.lockGetFormat.Unlock() + return mock.GetFormatFunc(pod) +} + +// GetFormatCalls gets all the calls that were made to GetFormat. +// Check the length with: +// +// len(mockedHTTPDataSourceStatService.GetFormatCalls()) +func (mock *HTTPDataSourceStatServiceMock) GetFormatCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetFormat.RLock() + calls = mock.calls.GetFormat + mock.lockGetFormat.RUnlock() + return calls +} + +// GetProgress calls GetProgressFunc. +func (mock *HTTPDataSourceStatServiceMock) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("HTTPDataSourceStatServiceMock.GetProgressFunc: method is nil but HTTPDataSourceStatService.GetProgress was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + }{ + OwnerUID: ownerUID, + Pod: pod, + PrevProgress: prevProgress, + Opts: opts, + } + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(ownerUID, pod, prevProgress, opts...) +} + +// GetProgressCalls gets all the calls that were made to GetProgress. +// Check the length with: +// +// len(mockedHTTPDataSourceStatService.GetProgressCalls()) +func (mock *HTTPDataSourceStatServiceMock) GetProgressCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + } + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() + return calls +} + +// GetSize calls GetSizeFunc. +func (mock *HTTPDataSourceStatServiceMock) GetSize(pod *corev1.Pod) v1alpha2.ImageStatusSize { + if mock.GetSizeFunc == nil { + panic("HTTPDataSourceStatServiceMock.GetSizeFunc: method is nil but HTTPDataSourceStatService.GetSize was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetSize.Lock() + mock.calls.GetSize = append(mock.calls.GetSize, callInfo) + mock.lockGetSize.Unlock() + return mock.GetSizeFunc(pod) +} + +// GetSizeCalls gets all the calls that were made to GetSize. +// Check the length with: +// +// len(mockedHTTPDataSourceStatService.GetSizeCalls()) +func (mock *HTTPDataSourceStatServiceMock) GetSizeCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetSize.RLock() + calls = mock.calls.GetSize + mock.lockGetSize.RUnlock() + return calls +} + +// Ensure, that RegistryDataSourceDiskServiceMock does implement RegistryDataSourceDiskService. +// If this is not the case, regenerate this file with moq. +var _ RegistryDataSourceDiskService = &RegistryDataSourceDiskServiceMock{} + +// RegistryDataSourceDiskServiceMock is a mock implementation of RegistryDataSourceDiskService. +// +// func TestSomethingThatUsesRegistryDataSourceDiskService(t *testing.T) { +// +// // make and configure a mocked RegistryDataSourceDiskService +// mockedRegistryDataSourceDiskService := &RegistryDataSourceDiskServiceMock{ +// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// CleanUpSupplementsFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUpSupplements method") +// }, +// EnsurePVCImportFunc: func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { +// panic("mock out the EnsurePVCImport method") +// }, +// GetCapacityFunc: func(pvc *corev1.PersistentVolumeClaim) string { +// panic("mock out the GetCapacity method") +// }, +// GetPersistentVolumeClaimFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { +// panic("mock out the GetPersistentVolumeClaim method") +// }, +// StartPVCImportFunc: func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { +// panic("mock out the StartPVCImport method") +// }, +// } +// +// // use mockedRegistryDataSourceDiskService in code that requires RegistryDataSourceDiskService +// // and then make assertions. +// +// } +type RegistryDataSourceDiskServiceMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // CleanUpSupplementsFunc mocks the CleanUpSupplements method. + CleanUpSupplementsFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // EnsurePVCImportFunc mocks the EnsurePVCImport method. + EnsurePVCImportFunc func(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) + + // GetCapacityFunc mocks the GetCapacity method. + GetCapacityFunc func(pvc *corev1.PersistentVolumeClaim) string + + // GetPersistentVolumeClaimFunc mocks the GetPersistentVolumeClaim method. + GetPersistentVolumeClaimFunc func(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) + + // StartPVCImportFunc mocks the StartPVCImport method. + StartPVCImportFunc func(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // CleanUpSupplements holds details about calls to the CleanUpSupplements method. + CleanUpSupplements []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // EnsurePVCImport holds details about calls to the EnsurePVCImport method. + EnsurePVCImport []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Target is the target argument value. + Target *corev1.PersistentVolumeClaim + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement + } + // GetCapacity holds details about calls to the GetCapacity method. + GetCapacity []struct { + // Pvc is the pvc argument value. + Pvc *corev1.PersistentVolumeClaim + } + // GetPersistentVolumeClaim holds details about calls to the GetPersistentVolumeClaim method. + GetPersistentVolumeClaim []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // StartPVCImport holds details about calls to the StartPVCImport method. + StartPVCImport []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // PvcSize is the pvcSize argument value. + PvcSize resource.Quantity + // Sc is the sc argument value. + Sc *v1.StorageClass + // Source is the source argument value. + Source *service.PVCImportSource + // Vd is the vd argument value. + Vd *v1alpha2.VirtualDisk + // NodePlacement is the nodePlacement argument value. + NodePlacement *provisioner.NodePlacement + } + } + lockCleanUp sync.RWMutex + lockCleanUpSupplements sync.RWMutex + lockEnsurePVCImport sync.RWMutex + lockGetCapacity sync.RWMutex + lockGetPersistentVolumeClaim sync.RWMutex + lockStartPVCImport sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *RegistryDataSourceDiskServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("RegistryDataSourceDiskServiceMock.CleanUpFunc: method is nil but RegistryDataSourceDiskService.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedRegistryDataSourceDiskService.CleanUpCalls()) +func (mock *RegistryDataSourceDiskServiceMock) CleanUpCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// CleanUpSupplements calls CleanUpSupplementsFunc. +func (mock *RegistryDataSourceDiskServiceMock) CleanUpSupplements(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpSupplementsFunc == nil { + panic("RegistryDataSourceDiskServiceMock.CleanUpSupplementsFunc: method is nil but RegistryDataSourceDiskService.CleanUpSupplements was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUpSupplements.Lock() + mock.calls.CleanUpSupplements = append(mock.calls.CleanUpSupplements, callInfo) + mock.lockCleanUpSupplements.Unlock() + return mock.CleanUpSupplementsFunc(ctx, sup) +} + +// CleanUpSupplementsCalls gets all the calls that were made to CleanUpSupplements. +// Check the length with: +// +// len(mockedRegistryDataSourceDiskService.CleanUpSupplementsCalls()) +func (mock *RegistryDataSourceDiskServiceMock) CleanUpSupplementsCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUpSupplements.RLock() + calls = mock.calls.CleanUpSupplements + mock.lockCleanUpSupplements.RUnlock() + return calls +} + +// EnsurePVCImport calls EnsurePVCImportFunc. +func (mock *RegistryDataSourceDiskServiceMock) EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) { + if mock.EnsurePVCImportFunc == nil { + panic("RegistryDataSourceDiskServiceMock.EnsurePVCImportFunc: method is nil but RegistryDataSourceDiskService.EnsurePVCImport was just called") + } + callInfo := struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + Target: target, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockEnsurePVCImport.Lock() + mock.calls.EnsurePVCImport = append(mock.calls.EnsurePVCImport, callInfo) + mock.lockEnsurePVCImport.Unlock() + return mock.EnsurePVCImportFunc(ctx, target, source, vd, nodePlacement) +} + +// EnsurePVCImportCalls gets all the calls that were made to EnsurePVCImport. +// Check the length with: +// +// len(mockedRegistryDataSourceDiskService.EnsurePVCImportCalls()) +func (mock *RegistryDataSourceDiskServiceMock) EnsurePVCImportCalls() []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + Target *corev1.PersistentVolumeClaim + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockEnsurePVCImport.RLock() + calls = mock.calls.EnsurePVCImport + mock.lockEnsurePVCImport.RUnlock() + return calls +} + +// GetCapacity calls GetCapacityFunc. +func (mock *RegistryDataSourceDiskServiceMock) GetCapacity(pvc *corev1.PersistentVolumeClaim) string { + if mock.GetCapacityFunc == nil { + panic("RegistryDataSourceDiskServiceMock.GetCapacityFunc: method is nil but RegistryDataSourceDiskService.GetCapacity was just called") + } + callInfo := struct { + Pvc *corev1.PersistentVolumeClaim + }{ + Pvc: pvc, + } + mock.lockGetCapacity.Lock() + mock.calls.GetCapacity = append(mock.calls.GetCapacity, callInfo) + mock.lockGetCapacity.Unlock() + return mock.GetCapacityFunc(pvc) +} + +// GetCapacityCalls gets all the calls that were made to GetCapacity. +// Check the length with: +// +// len(mockedRegistryDataSourceDiskService.GetCapacityCalls()) +func (mock *RegistryDataSourceDiskServiceMock) GetCapacityCalls() []struct { + Pvc *corev1.PersistentVolumeClaim +} { + var calls []struct { + Pvc *corev1.PersistentVolumeClaim + } + mock.lockGetCapacity.RLock() + calls = mock.calls.GetCapacity + mock.lockGetCapacity.RUnlock() + return calls +} + +// GetPersistentVolumeClaim calls GetPersistentVolumeClaimFunc. +func (mock *RegistryDataSourceDiskServiceMock) GetPersistentVolumeClaim(ctx context.Context, sup supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + if mock.GetPersistentVolumeClaimFunc == nil { + panic("RegistryDataSourceDiskServiceMock.GetPersistentVolumeClaimFunc: method is nil but RegistryDataSourceDiskService.GetPersistentVolumeClaim was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPersistentVolumeClaim.Lock() + mock.calls.GetPersistentVolumeClaim = append(mock.calls.GetPersistentVolumeClaim, callInfo) + mock.lockGetPersistentVolumeClaim.Unlock() + return mock.GetPersistentVolumeClaimFunc(ctx, sup) +} + +// GetPersistentVolumeClaimCalls gets all the calls that were made to GetPersistentVolumeClaim. +// Check the length with: +// +// len(mockedRegistryDataSourceDiskService.GetPersistentVolumeClaimCalls()) +func (mock *RegistryDataSourceDiskServiceMock) GetPersistentVolumeClaimCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetPersistentVolumeClaim.RLock() + calls = mock.calls.GetPersistentVolumeClaim + mock.lockGetPersistentVolumeClaim.RUnlock() + return calls +} + +// StartPVCImport calls StartPVCImportFunc. +func (mock *RegistryDataSourceDiskServiceMock) StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *v1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error { + if mock.StartPVCImportFunc == nil { + panic("RegistryDataSourceDiskServiceMock.StartPVCImportFunc: method is nil but RegistryDataSourceDiskService.StartPVCImport was just called") + } + callInfo := struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + }{ + Ctx: ctx, + PvcSize: pvcSize, + Sc: sc, + Source: source, + Vd: vd, + NodePlacement: nodePlacement, + } + mock.lockStartPVCImport.Lock() + mock.calls.StartPVCImport = append(mock.calls.StartPVCImport, callInfo) + mock.lockStartPVCImport.Unlock() + return mock.StartPVCImportFunc(ctx, pvcSize, sc, source, vd, nodePlacement) +} + +// StartPVCImportCalls gets all the calls that were made to StartPVCImport. +// Check the length with: +// +// len(mockedRegistryDataSourceDiskService.StartPVCImportCalls()) +func (mock *RegistryDataSourceDiskServiceMock) StartPVCImportCalls() []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement +} { + var calls []struct { + Ctx context.Context + PvcSize resource.Quantity + Sc *v1.StorageClass + Source *service.PVCImportSource + Vd *v1alpha2.VirtualDisk + NodePlacement *provisioner.NodePlacement + } + mock.lockStartPVCImport.RLock() + calls = mock.calls.StartPVCImport + mock.lockStartPVCImport.RUnlock() + return calls +} + +// Ensure, that RegistryDataSourceImporterServiceMock does implement RegistryDataSourceImporterService. +// If this is not the case, regenerate this file with moq. +var _ RegistryDataSourceImporterService = &RegistryDataSourceImporterServiceMock{} + +// RegistryDataSourceImporterServiceMock is a mock implementation of RegistryDataSourceImporterService. +// +// func TestSomethingThatUsesRegistryDataSourceImporterService(t *testing.T) { +// +// // make and configure a mocked RegistryDataSourceImporterService +// mockedRegistryDataSourceImporterService := &RegistryDataSourceImporterServiceMock{ +// CleanUpFunc: func(ctx context.Context, sup supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// GetPodFunc: func(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) { +// panic("mock out the GetPod method") +// }, +// ProtectFunc: func(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error { +// panic("mock out the Protect method") +// }, +// StartFunc: func(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { +// panic("mock out the Start method") +// }, +// } +// +// // use mockedRegistryDataSourceImporterService in code that requires RegistryDataSourceImporterService +// // and then make assertions. +// +// } +type RegistryDataSourceImporterServiceMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup supplements.Generator) (bool, error) + + // GetPodFunc mocks the GetPod method. + GetPodFunc func(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) + + // ProtectFunc mocks the Protect method. + ProtectFunc func(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // GetPod holds details about calls to the GetPod method. + GetPod []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup supplements.Generator + } + // Protect holds details about calls to the Protect method. + Protect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Pod is the pod argument value. + Pod *corev1.Pod + // Sup is the sup argument value. + Sup supplements.Generator + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Settings is the settings argument value. + Settings *importer.Settings + // Obj is the obj argument value. + Obj client.Object + // Sup is the sup argument value. + Sup supplements.Generator + // CaBundle is the caBundle argument value. + CaBundle *datasource.CABundle + // Opts is the opts argument value. + Opts []service.Option + } + } + lockCleanUp sync.RWMutex + lockGetPod sync.RWMutex + lockProtect sync.RWMutex + lockStart sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *RegistryDataSourceImporterServiceMock) CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("RegistryDataSourceImporterServiceMock.CleanUpFunc: method is nil but RegistryDataSourceImporterService.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedRegistryDataSourceImporterService.CleanUpCalls()) +func (mock *RegistryDataSourceImporterServiceMock) CleanUpCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// GetPod calls GetPodFunc. +func (mock *RegistryDataSourceImporterServiceMock) GetPod(ctx context.Context, sup supplements.Generator) (*corev1.Pod, error) { + if mock.GetPodFunc == nil { + panic("RegistryDataSourceImporterServiceMock.GetPodFunc: method is nil but RegistryDataSourceImporterService.GetPod was just called") + } + callInfo := struct { + Ctx context.Context + Sup supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPod.Lock() + mock.calls.GetPod = append(mock.calls.GetPod, callInfo) + mock.lockGetPod.Unlock() + return mock.GetPodFunc(ctx, sup) +} + +// GetPodCalls gets all the calls that were made to GetPod. +// Check the length with: +// +// len(mockedRegistryDataSourceImporterService.GetPodCalls()) +func (mock *RegistryDataSourceImporterServiceMock) GetPodCalls() []struct { + Ctx context.Context + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup supplements.Generator + } + mock.lockGetPod.RLock() + calls = mock.calls.GetPod + mock.lockGetPod.RUnlock() + return calls +} + +// Protect calls ProtectFunc. +func (mock *RegistryDataSourceImporterServiceMock) Protect(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error { + if mock.ProtectFunc == nil { + panic("RegistryDataSourceImporterServiceMock.ProtectFunc: method is nil but RegistryDataSourceImporterService.Protect was just called") + } + callInfo := struct { + Ctx context.Context + Pod *corev1.Pod + Sup supplements.Generator + }{ + Ctx: ctx, + Pod: pod, + Sup: sup, + } + mock.lockProtect.Lock() + mock.calls.Protect = append(mock.calls.Protect, callInfo) + mock.lockProtect.Unlock() + return mock.ProtectFunc(ctx, pod, sup) +} + +// ProtectCalls gets all the calls that were made to Protect. +// Check the length with: +// +// len(mockedRegistryDataSourceImporterService.ProtectCalls()) +func (mock *RegistryDataSourceImporterServiceMock) ProtectCalls() []struct { + Ctx context.Context + Pod *corev1.Pod + Sup supplements.Generator +} { + var calls []struct { + Ctx context.Context + Pod *corev1.Pod + Sup supplements.Generator + } + mock.lockProtect.RLock() + calls = mock.calls.Protect + mock.lockProtect.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *RegistryDataSourceImporterServiceMock) Start(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { + if mock.StartFunc == nil { + panic("RegistryDataSourceImporterServiceMock.StartFunc: method is nil but RegistryDataSourceImporterService.Start was just called") + } + callInfo := struct { + Ctx context.Context + Settings *importer.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option + }{ + Ctx: ctx, + Settings: settings, + Obj: obj, + Sup: sup, + CaBundle: caBundle, + Opts: opts, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + return mock.StartFunc(ctx, settings, obj, sup, caBundle, opts...) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// +// len(mockedRegistryDataSourceImporterService.StartCalls()) +func (mock *RegistryDataSourceImporterServiceMock) StartCalls() []struct { + Ctx context.Context + Settings *importer.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option +} { + var calls []struct { + Ctx context.Context + Settings *importer.Settings + Obj client.Object + Sup supplements.Generator + CaBundle *datasource.CABundle + Opts []service.Option + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} + +// Ensure, that RegistryDataSourceStatServiceMock does implement RegistryDataSourceStatService. +// If this is not the case, regenerate this file with moq. +var _ RegistryDataSourceStatService = &RegistryDataSourceStatServiceMock{} + +// RegistryDataSourceStatServiceMock is a mock implementation of RegistryDataSourceStatService. +// +// func TestSomethingThatUsesRegistryDataSourceStatService(t *testing.T) { +// +// // make and configure a mocked RegistryDataSourceStatService +// mockedRegistryDataSourceStatService := &RegistryDataSourceStatServiceMock{ +// CheckPodFunc: func(pod *corev1.Pod) error { +// panic("mock out the CheckPod method") +// }, +// GetDVCRImageNameFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetDVCRImageName method") +// }, +// GetDownloadSpeedFunc: func(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed { +// panic("mock out the GetDownloadSpeed method") +// }, +// GetFormatFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetFormat method") +// }, +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// GetSizeFunc: func(pod *corev1.Pod) v1alpha2.ImageStatusSize { +// panic("mock out the GetSize method") +// }, +// } +// +// // use mockedRegistryDataSourceStatService in code that requires RegistryDataSourceStatService +// // and then make assertions. +// +// } +type RegistryDataSourceStatServiceMock struct { + // CheckPodFunc mocks the CheckPod method. + CheckPodFunc func(pod *corev1.Pod) error + + // GetDVCRImageNameFunc mocks the GetDVCRImageName method. + GetDVCRImageNameFunc func(pod *corev1.Pod) string + + // GetDownloadSpeedFunc mocks the GetDownloadSpeed method. + GetDownloadSpeedFunc func(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed + + // GetFormatFunc mocks the GetFormat method. + GetFormatFunc func(pod *corev1.Pod) string + + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + + // GetSizeFunc mocks the GetSize method. + GetSizeFunc func(pod *corev1.Pod) v1alpha2.ImageStatusSize + + // calls tracks calls to the methods. + calls struct { + // CheckPod holds details about calls to the CheckPod method. + CheckPod []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDVCRImageName holds details about calls to the GetDVCRImageName method. + GetDVCRImageName []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDownloadSpeed holds details about calls to the GetDownloadSpeed method. + GetDownloadSpeed []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetFormat holds details about calls to the GetFormat method. + GetFormat []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } + // GetSize holds details about calls to the GetSize method. + GetSize []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + } + lockCheckPod sync.RWMutex + lockGetDVCRImageName sync.RWMutex + lockGetDownloadSpeed sync.RWMutex + lockGetFormat sync.RWMutex + lockGetProgress sync.RWMutex + lockGetSize sync.RWMutex +} + +// CheckPod calls CheckPodFunc. +func (mock *RegistryDataSourceStatServiceMock) CheckPod(pod *corev1.Pod) error { + if mock.CheckPodFunc == nil { + panic("RegistryDataSourceStatServiceMock.CheckPodFunc: method is nil but RegistryDataSourceStatService.CheckPod was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockCheckPod.Lock() + mock.calls.CheckPod = append(mock.calls.CheckPod, callInfo) + mock.lockCheckPod.Unlock() + return mock.CheckPodFunc(pod) +} + +// CheckPodCalls gets all the calls that were made to CheckPod. +// Check the length with: +// +// len(mockedRegistryDataSourceStatService.CheckPodCalls()) +func (mock *RegistryDataSourceStatServiceMock) CheckPodCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockCheckPod.RLock() + calls = mock.calls.CheckPod + mock.lockCheckPod.RUnlock() + return calls +} + +// GetDVCRImageName calls GetDVCRImageNameFunc. +func (mock *RegistryDataSourceStatServiceMock) GetDVCRImageName(pod *corev1.Pod) string { + if mock.GetDVCRImageNameFunc == nil { + panic("RegistryDataSourceStatServiceMock.GetDVCRImageNameFunc: method is nil but RegistryDataSourceStatService.GetDVCRImageName was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetDVCRImageName.Lock() + mock.calls.GetDVCRImageName = append(mock.calls.GetDVCRImageName, callInfo) + mock.lockGetDVCRImageName.Unlock() + return mock.GetDVCRImageNameFunc(pod) +} + +// GetDVCRImageNameCalls gets all the calls that were made to GetDVCRImageName. +// Check the length with: +// +// len(mockedRegistryDataSourceStatService.GetDVCRImageNameCalls()) +func (mock *RegistryDataSourceStatServiceMock) GetDVCRImageNameCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetDVCRImageName.RLock() + calls = mock.calls.GetDVCRImageName + mock.lockGetDVCRImageName.RUnlock() + return calls +} + +// GetDownloadSpeed calls GetDownloadSpeedFunc. +func (mock *RegistryDataSourceStatServiceMock) GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed { + if mock.GetDownloadSpeedFunc == nil { + panic("RegistryDataSourceStatServiceMock.GetDownloadSpeedFunc: method is nil but RegistryDataSourceStatService.GetDownloadSpeed was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + }{ + OwnerUID: ownerUID, + Pod: pod, + } + mock.lockGetDownloadSpeed.Lock() + mock.calls.GetDownloadSpeed = append(mock.calls.GetDownloadSpeed, callInfo) + mock.lockGetDownloadSpeed.Unlock() + return mock.GetDownloadSpeedFunc(ownerUID, pod) +} + +// GetDownloadSpeedCalls gets all the calls that were made to GetDownloadSpeed. +// Check the length with: +// +// len(mockedRegistryDataSourceStatService.GetDownloadSpeedCalls()) +func (mock *RegistryDataSourceStatServiceMock) GetDownloadSpeedCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + } + mock.lockGetDownloadSpeed.RLock() + calls = mock.calls.GetDownloadSpeed + mock.lockGetDownloadSpeed.RUnlock() + return calls +} + +// GetFormat calls GetFormatFunc. +func (mock *RegistryDataSourceStatServiceMock) GetFormat(pod *corev1.Pod) string { + if mock.GetFormatFunc == nil { + panic("RegistryDataSourceStatServiceMock.GetFormatFunc: method is nil but RegistryDataSourceStatService.GetFormat was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetFormat.Lock() + mock.calls.GetFormat = append(mock.calls.GetFormat, callInfo) + mock.lockGetFormat.Unlock() + return mock.GetFormatFunc(pod) +} + +// GetFormatCalls gets all the calls that were made to GetFormat. +// Check the length with: +// +// len(mockedRegistryDataSourceStatService.GetFormatCalls()) +func (mock *RegistryDataSourceStatServiceMock) GetFormatCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetFormat.RLock() + calls = mock.calls.GetFormat + mock.lockGetFormat.RUnlock() + return calls +} + +// GetProgress calls GetProgressFunc. +func (mock *RegistryDataSourceStatServiceMock) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("RegistryDataSourceStatServiceMock.GetProgressFunc: method is nil but RegistryDataSourceStatService.GetProgress was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + }{ + OwnerUID: ownerUID, + Pod: pod, + PrevProgress: prevProgress, + Opts: opts, + } + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(ownerUID, pod, prevProgress, opts...) +} + +// GetProgressCalls gets all the calls that were made to GetProgress. +// Check the length with: +// +// len(mockedRegistryDataSourceStatService.GetProgressCalls()) +func (mock *RegistryDataSourceStatServiceMock) GetProgressCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + } + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() + return calls +} + +// GetSize calls GetSizeFunc. +func (mock *RegistryDataSourceStatServiceMock) GetSize(pod *corev1.Pod) v1alpha2.ImageStatusSize { + if mock.GetSizeFunc == nil { + panic("RegistryDataSourceStatServiceMock.GetSizeFunc: method is nil but RegistryDataSourceStatService.GetSize was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetSize.Lock() + mock.calls.GetSize = append(mock.calls.GetSize, callInfo) + mock.lockGetSize.Unlock() + return mock.GetSizeFunc(pod) +} + +// GetSizeCalls gets all the calls that were made to GetSize. +// Check the length with: +// +// len(mockedRegistryDataSourceStatService.GetSizeCalls()) +func (mock *RegistryDataSourceStatServiceMock) GetSizeCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetSize.RLock() + calls = mock.calls.GetSize + mock.lockGetSize.RUnlock() return calls } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go index a3586326cd..b7d483fb13 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go @@ -40,14 +40,15 @@ type ObjectRefDataSource struct { func NewObjectRefDataSource( recorder eventrecord.EventRecorderLogger, + statService *service.StatService, diskService *service.DiskService, client client.Client, ) *ObjectRefDataSource { return &ObjectRefDataSource{ diskService: diskService, vdSnapshotSyncer: NewObjectRefVirtualDiskSnapshot(recorder, diskService, client), - viSyncer: NewObjectRefVirtualImage(diskService, client), - cviSyncer: NewObjectRefClusterVirtualImage(diskService, client), + viSyncer: NewObjectRefVirtualImage(diskService, statService, client), + cviSyncer: NewObjectRefClusterVirtualImage(diskService, statService, client), } } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go index bc4975dcef..a68412d18d 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi.go @@ -23,13 +23,13 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/common/steptaker" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -38,15 +38,18 @@ import ( type ObjectRefClusterVirtualImage struct { diskService ObjectRefClusterVirtualImageDiskService + statService ObjectRefClusterVirtualImageStatService client client.Client } func NewObjectRefClusterVirtualImage( diskService ObjectRefClusterVirtualImageDiskService, + statService ObjectRefClusterVirtualImageStatService, client client.Client, ) *ObjectRefClusterVirtualImage { return &ObjectRefClusterVirtualImage{ diskService: diskService, + statService: statService, client: client, } } @@ -66,18 +69,22 @@ func (ds ObjectRefClusterVirtualImage) Sync(ctx context.Context, vd *v1alpha2.Vi return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) } - dv, err := object.FetchObject(ctx, supgen.DataVolume(), ds.client, &cdiv1.DataVolume{}) + cviRefKey := types.NamespacedName{Name: vd.Spec.DataSource.ObjectRef.Name} + cviRef, err := object.FetchObject(ctx, cviRefKey, ds.client, &v1alpha2.ClusterVirtualImage{}) if err != nil { - return reconcile.Result{}, fmt.Errorf("fetch dv: %w", err) + return reconcile.Result{}, fmt.Errorf("fetch cvi %q: %w", cviRefKey, err) + } + var importSource *service.PVCImportSource + if cviRef != nil { + importSource = step.BuildClusterVirtualImagePVCImportSource(vd, cviRef) } return steptaker.NewStepTakers[*v1alpha2.VirtualDisk]( step.NewReadyStep(ds.diskService, pvc, cb), step.NewTerminatingStep(pvc), - step.NewCreateDataVolumeFromClusterVirtualImageStep(pvc, dv, ds.diskService, ds.client, cb), - step.NewEnsureNodePlacementStep(pvc, dv, ds.diskService, ds.client, cb), - step.NewWaitForDVStep(pvc, dv, ds.diskService, ds.client, cb), + step.NewPVCImportFromClusterVirtualImageStep(pvc, ds.diskService, ds.client, cb), step.NewWaitForPVCStep(pvc, ds.client, cb), + step.NewWaitForPVCImportStep(pvc, step.StaticPVCImportSource(importSource), ds.diskService, ds.statService, nil, ds.client, cb), ).Run(ctx, vd) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi_test.go index bba5d04a43..b10072e953 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_cvi_test.go @@ -27,11 +27,12 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" @@ -48,8 +49,8 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { vd *v1alpha2.VirtualDisk sc *storagev1.StorageClass pvc *corev1.PersistentVolumeClaim - dv *cdiv1.DataVolume svc *ObjectRefVirtualImageDiskServiceMock + stat *ObjectRefClusterVirtualImageStatServiceMock ) BeforeEach(func() { @@ -58,21 +59,23 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { scheme = runtime.NewScheme() Expect(v1alpha2.AddToScheme(scheme)).To(Succeed()) Expect(corev1.AddToScheme(scheme)).To(Succeed()) - Expect(cdiv1.AddToScheme(scheme)).To(Succeed()) Expect(storagev1.AddToScheme(scheme)).To(Succeed()) - svc = &ObjectRefVirtualImageDiskServiceMock{ - GetProgressFunc: func(_ *cdiv1.DataVolume, _ string, _ ...service.GetProgressOption) string { - return "10%" + stat = &ObjectRefClusterVirtualImageStatServiceMock{ + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, prev string, _ ...service.GetProgressOption) string { + return prev }, + } + + svc = &ObjectRefVirtualImageDiskServiceMock{ GetCapacityFunc: func(_ *corev1.PersistentVolumeClaim) string { return "100Mi" }, CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, - ProtectFunc: func(_ context.Context, _ supplements.Generator, _ client.Object, _ *cdiv1.DataVolume, _ *corev1.PersistentVolumeClaim) error { - return nil + EnsurePVCImportFunc: func(_ context.Context, _ *corev1.PersistentVolumeClaim, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) (corev1.PodPhase, error) { + return corev1.PodRunning, nil }, } @@ -134,39 +137,29 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { }, }, } - - dv = &cdiv1.DataVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: supgen.DataVolume().Name, - Namespace: vd.Namespace, - }, - Status: cdiv1.DataVolumeStatus{ - ClaimName: pvc.Name, - }, - } }) Context("VirtualDisk has just been created", func() { - It("must create DataVolume", func() { - var dvCreated bool + It("must start PVC import", func() { + var importStarted bool vd.Status = v1alpha2.VirtualDiskStatus{ Target: v1alpha2.DiskTarget{ PersistentVolumeClaim: "test-pvc", }, } fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cvi, sc).Build() - svc.StartFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, _ *cdiv1.DataVolumeSource, _ client.Object, _ supplements.DataVolumeSupplement, _ ...service.Option) error { - dvCreated = true + svc.StartPVCImportFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) error { + importStarted = true return nil } - syncer := NewObjectRefClusterVirtualImage(svc, fakeClient) + syncer := NewObjectRefClusterVirtualImage(svc, stat, fakeClient) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) Expect(res.IsZero()).To(BeTrue()) - Expect(dvCreated).To(BeTrue()) + Expect(importStarted).To(BeTrue()) ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) @@ -176,25 +169,12 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { }) Context("VirtualDisk waits for the PVC to be Bound", func() { - BeforeEach(func() { - svc.CheckProvisioningFunc = func(_ context.Context, _ *corev1.PersistentVolumeClaim) error { - return nil - } - }) - It("waits for the first consumer", func() { - dv.Status.Phase = cdiv1.PendingPopulation - dv.Status.Conditions = []cdiv1.DataVolumeCondition{ - { - Type: cdiv1.DataVolumeRunning, - Status: corev1.ConditionFalse, - Reason: "", - }, - } + pvc.Status.Phase = corev1.ClaimPending sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingWaitForFirstConsumer) - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, dv, sc).Build() + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() - syncer := NewObjectRefClusterVirtualImage(svc, client) + syncer := NewObjectRefClusterVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -209,9 +189,9 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { It("is in provisioning", func() { pvc.Status.Phase = corev1.ClaimPending sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingImmediate) - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, dv, sc).Build() + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() - syncer := NewObjectRefClusterVirtualImage(svc, client) + syncer := NewObjectRefClusterVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -226,11 +206,10 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { Context("VirtualDisk is ready", func() { It("checks that the VirtualDisk is ready", func() { - dv.Status.Phase = cdiv1.Succeeded pvc.Status.Phase = corev1.ClaimBound - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(dv, pvc).Build() + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() - syncer := NewObjectRefClusterVirtualImage(svc, client) + syncer := NewObjectRefClusterVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -240,6 +219,23 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskReady)) ExpectStats(vd) }) + + It("requeues when the import has just completed", func() { + pvc.Status.Phase = corev1.ClaimBound + pvc.Annotations = map[string]string{ + annotations.AnnPVCImportPhase: string(corev1.PodRunning), + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, cvi).Build() + svc.EnsurePVCImportFunc = func(_ context.Context, _ *corev1.PersistentVolumeClaim, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) (corev1.PodPhase, error) { + return corev1.PodSucceeded, nil + } + + syncer := NewObjectRefClusterVirtualImage(svc, stat, client) + + res, err := syncer.Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + }) }) Context("VirtualDisk is lost", func() { @@ -258,7 +254,7 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { } client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - syncer := NewObjectRefClusterVirtualImage(svc, client) + syncer := NewObjectRefClusterVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -274,7 +270,7 @@ var _ = Describe("ObjectRef ClusterVirtualImage", func() { vd.Status.Target.PersistentVolumeClaim = pvc.Name client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() - syncer := NewObjectRefClusterVirtualImage(svc, client) + syncer := NewObjectRefClusterVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go index 446aedc7ae..2c6ff5016b 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go @@ -30,7 +30,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" @@ -82,9 +81,6 @@ var _ = Describe("ObjectRef VirtualDiskSnapshot", func() { CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, - ProtectFunc: func(_ context.Context, _ supplements.Generator, _ client.Object, _ *cdiv1.DataVolume, _ *corev1.PersistentVolumeClaim) error { - return nil - }, } sc = &storagev1.StorageClass{ diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go index ecf5891a25..1f26c266cc 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi.go @@ -23,13 +23,13 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/common/steptaker" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -38,15 +38,18 @@ import ( type ObjectRefVirtualImage struct { diskService ObjectRefVirtualImageDiskService + statService ObjectRefVirtualImageStatService client client.Client } func NewObjectRefVirtualImage( diskService ObjectRefVirtualImageDiskService, + statService ObjectRefVirtualImageStatService, client client.Client, ) *ObjectRefVirtualImage { return &ObjectRefVirtualImage{ diskService: diskService, + statService: statService, client: client, } } @@ -66,18 +69,25 @@ func (ds ObjectRefVirtualImage) Sync(ctx context.Context, vd *v1alpha2.VirtualDi return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) } - dv, err := object.FetchObject(ctx, supgen.DataVolume(), ds.client, &cdiv1.DataVolume{}) + viRefKey := types.NamespacedName{Name: vd.Spec.DataSource.ObjectRef.Name, Namespace: vd.Namespace} + viRef, err := object.FetchObject(ctx, viRefKey, ds.client, &v1alpha2.VirtualImage{}) if err != nil { - return reconcile.Result{}, fmt.Errorf("fetch dv: %w", err) + return reconcile.Result{}, fmt.Errorf("fetch vi %q: %w", viRefKey, err) + } + var importSource *service.PVCImportSource + if viRef != nil { + importSource, err = step.BuildVirtualImagePVCImportSource(vd, viRef) + if err != nil { + return reconcile.Result{}, fmt.Errorf("build import source: %w", err) + } } return steptaker.NewStepTakers[*v1alpha2.VirtualDisk]( step.NewReadyStep(ds.diskService, pvc, cb), step.NewTerminatingStep(pvc), - step.NewCreateDataVolumeFromVirtualImageStep(pvc, dv, ds.diskService, ds.client, cb), - step.NewEnsureNodePlacementStep(pvc, dv, ds.diskService, ds.client, cb), - step.NewWaitForDVStep(pvc, dv, ds.diskService, ds.client, cb), + step.NewPVCImportFromVirtualImageStep(pvc, ds.diskService, ds.client, cb), step.NewWaitForPVCStep(pvc, ds.client, cb), + step.NewWaitForPVCImportStep(pvc, step.StaticPVCImportSource(importSource), ds.diskService, ds.statService, nil, ds.client, cb), ).Run(ctx, vd) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi_test.go index 5eacabf594..c377c7e6af 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vi_test.go @@ -27,11 +27,12 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" @@ -48,8 +49,8 @@ var _ = Describe("ObjectRef VirtualImage", func() { vd *v1alpha2.VirtualDisk sc *storagev1.StorageClass pvc *corev1.PersistentVolumeClaim - dv *cdiv1.DataVolume svc *ObjectRefVirtualImageDiskServiceMock + stat *ObjectRefVirtualImageStatServiceMock ) BeforeEach(func() { @@ -58,21 +59,23 @@ var _ = Describe("ObjectRef VirtualImage", func() { scheme = runtime.NewScheme() Expect(v1alpha2.AddToScheme(scheme)).To(Succeed()) Expect(corev1.AddToScheme(scheme)).To(Succeed()) - Expect(cdiv1.AddToScheme(scheme)).To(Succeed()) Expect(storagev1.AddToScheme(scheme)).To(Succeed()) - svc = &ObjectRefVirtualImageDiskServiceMock{ - GetProgressFunc: func(_ *cdiv1.DataVolume, _ string, _ ...service.GetProgressOption) string { - return "10%" + stat = &ObjectRefVirtualImageStatServiceMock{ + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, prev string, _ ...service.GetProgressOption) string { + return prev }, + } + + svc = &ObjectRefVirtualImageDiskServiceMock{ GetCapacityFunc: func(_ *corev1.PersistentVolumeClaim) string { return "100Mi" }, CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, - ProtectFunc: func(_ context.Context, _ supplements.Generator, _ client.Object, _ *cdiv1.DataVolume, _ *corev1.PersistentVolumeClaim) error { - return nil + EnsurePVCImportFunc: func(_ context.Context, _ *corev1.PersistentVolumeClaim, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) (corev1.PodPhase, error) { + return corev1.PodRunning, nil }, } @@ -134,39 +137,29 @@ var _ = Describe("ObjectRef VirtualImage", func() { }, }, } - - dv = &cdiv1.DataVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: supgen.DataVolume().Name, - Namespace: vd.Namespace, - }, - Status: cdiv1.DataVolumeStatus{ - ClaimName: pvc.Name, - }, - } }) Context("VirtualDisk has just been created", func() { - It("must create DataVolume", func() { - var dvCreated bool + It("must start PVC import", func() { + var importStarted bool vd.Status = v1alpha2.VirtualDiskStatus{ Target: v1alpha2.DiskTarget{ PersistentVolumeClaim: "test-pvc", }, } fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vi, sc).Build() - svc.StartFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, _ *cdiv1.DataVolumeSource, _ client.Object, _ supplements.DataVolumeSupplement, _ ...service.Option) error { - dvCreated = true + svc.StartPVCImportFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) error { + importStarted = true return nil } - syncer := NewObjectRefVirtualImage(svc, fakeClient) + syncer := NewObjectRefVirtualImage(svc, stat, fakeClient) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) Expect(res.IsZero()).To(BeTrue()) - Expect(dvCreated).To(BeTrue()) + Expect(importStarted).To(BeTrue()) ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) @@ -176,25 +169,12 @@ var _ = Describe("ObjectRef VirtualImage", func() { }) Context("VirtualDisk waits for the PVC to be Bound", func() { - BeforeEach(func() { - svc.CheckProvisioningFunc = func(_ context.Context, _ *corev1.PersistentVolumeClaim) error { - return nil - } - }) - It("waits for the first consumer", func() { - dv.Status.Phase = cdiv1.PendingPopulation - dv.Status.Conditions = []cdiv1.DataVolumeCondition{ - { - Type: cdiv1.DataVolumeRunning, - Status: corev1.ConditionFalse, - Reason: "", - }, - } + pvc.Status.Phase = corev1.ClaimPending sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingWaitForFirstConsumer) - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, dv, sc).Build() + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() - syncer := NewObjectRefVirtualImage(svc, client) + syncer := NewObjectRefVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -209,9 +189,9 @@ var _ = Describe("ObjectRef VirtualImage", func() { It("is in provisioning", func() { pvc.Status.Phase = corev1.ClaimPending sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingImmediate) - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, dv, sc).Build() + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() - syncer := NewObjectRefVirtualImage(svc, client) + syncer := NewObjectRefVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -226,11 +206,10 @@ var _ = Describe("ObjectRef VirtualImage", func() { Context("VirtualDisk is ready", func() { It("checks that the VirtualDisk is ready", func() { - dv.Status.Phase = cdiv1.Succeeded pvc.Status.Phase = corev1.ClaimBound - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(dv, pvc).Build() + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() - syncer := NewObjectRefVirtualImage(svc, client) + syncer := NewObjectRefVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -240,6 +219,23 @@ var _ = Describe("ObjectRef VirtualImage", func() { Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskReady)) ExpectStats(vd) }) + + It("requeues when the import has just completed", func() { + pvc.Status.Phase = corev1.ClaimBound + pvc.Annotations = map[string]string{ + annotations.AnnPVCImportPhase: string(corev1.PodRunning), + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, vi).Build() + svc.EnsurePVCImportFunc = func(_ context.Context, _ *corev1.PersistentVolumeClaim, _ *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) (corev1.PodPhase, error) { + return corev1.PodSucceeded, nil + } + + syncer := NewObjectRefVirtualImage(svc, stat, client) + + res, err := syncer.Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + }) }) Context("VirtualDisk is lost", func() { @@ -258,7 +254,7 @@ var _ = Describe("ObjectRef VirtualImage", func() { } client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - syncer := NewObjectRefVirtualImage(svc, client) + syncer := NewObjectRefVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) @@ -274,7 +270,7 @@ var _ = Describe("ObjectRef VirtualImage", func() { vd.Status.Target.PersistentVolumeClaim = pvc.Name client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() - syncer := NewObjectRefVirtualImage(svc, client) + syncer := NewObjectRefVirtualImage(svc, stat, client) res, err := syncer.Sync(ctx, vd) Expect(err).ToNot(HaveOccurred()) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go index 3b67262298..c7c6d7b521 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go @@ -20,28 +20,20 @@ import ( "context" "errors" "fmt" - "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/deckhouse/virtualization-controller/pkg/common" "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" "github.com/deckhouse/virtualization-controller/pkg/common/object" - podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" - "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/common/steptaker" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/importer" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" @@ -53,9 +45,9 @@ import ( const registryDataSource = "registry" type RegistryDataSource struct { - statService *service.StatService - importerService *service.ImporterService - diskService *service.DiskService + statService RegistryDataSourceStatService + importerService RegistryDataSourceImporterService + diskService RegistryDataSourceDiskService dvcrSettings *dvcr.Settings client client.Client recorder eventrecord.EventRecorderLogger @@ -63,9 +55,9 @@ type RegistryDataSource struct { func NewRegistryDataSource( recorder eventrecord.EventRecorderLogger, - statService *service.StatService, - importerService *service.ImporterService, - diskService *service.DiskService, + statService RegistryDataSourceStatService, + importerService RegistryDataSourceImporterService, + diskService RegistryDataSourceDiskService, dvcrSettings *dvcr.Settings, client client.Client, ) *RegistryDataSource { @@ -82,273 +74,34 @@ func NewRegistryDataSource( func (ds RegistryDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { log, ctx := logger.GetDataSourceContext(ctx, registryDataSource) - condition, _ := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + supgen := vdsupplements.NewGenerator(vd) + cb := conditions.NewConditionBuilder(vdcondition.ReadyType).Generation(vd.Generation) defer func() { conditions.SetCondition(cb, &vd.Status.Conditions) }() - supgen := vdsupplements.NewGenerator(vd) - pod, err := ds.importerService.GetPod(ctx, supgen) if err != nil { - return reconcile.Result{}, err - } - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) + return reconcile.Result{}, fmt.Errorf("fetch importer pod: %w", err) } - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vd.Status.StorageClassName) + pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { - return reconcile.Result{}, err - } - - switch { - case IsDiskProvisioningFinished(condition): - log.Debug("Disk provisioning finished: clean up") - - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) - - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vd, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - // Unprotect import time supplements to delete them later. - err = ds.importerService.Unprotect(ctx, pod, supgen) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, supgen, dv) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vd, ds) - case object.AnyTerminating(pod, dv, pvc): - log.Info("Waiting for supplements to be terminated") - case pod == nil: - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncStarted, - "The Registry DataSource import to DVCR has started", - ) - - vd.Status.Progress = "0%" - - envSettings := ds.getEnvSettings(vd, supgen) - - err = ds.importerService.Start( - ctx, envSettings, vd, supgen, - datasource.NewCABundleForVMD(vd.GetNamespace(), vd.Spec.DataSource), - service.WithSystemNodeToleration(), - ) - switch { - case err == nil: - // OK. - case common.ErrQuotaExceeded(err): - ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil - default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, err - } - - vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitForUserUpload). - Message("DVCR Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case !podutil.IsPodComplete(pod): - log.Info("Provisioning to DVCR is in progress", "podPhase", pod.Status.Phase) - - err = ds.statService.CheckPod(pod) - if err != nil { - return reconcile.Result{}, setPhaseConditionFromPodError(ctx, err, pod, vd, cb, ds.client) - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") - - vd.Status.Progress = ds.statService.GetProgress(vd.GetUID(), pod, vd.Status.Progress, service.NewScaleOption(0, 50)) - - err = ds.importerService.Protect(ctx, pod, supgen) - if err != nil { - return reconcile.Result{}, err - } - case dv == nil: - if isStorageClassWFFC(sc) && len(vd.Status.AttachedToVirtualMachines) != 1 { - vd.Status.Progress = "50%" - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - return reconcile.Result{}, nil - } - - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncStarted, - "The Registry DataSource import to PVC has started", - ) - - err = ds.statService.CheckPod(pod) - if err != nil { - vd.Status.Phase = v1alpha2.DiskFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - vd.Status.Progress = "50%" - - if imageformat.IsISO(ds.statService.GetFormat(pod)) { - setPhaseConditionToFailed(cb, &vd.Status.Phase, ErrISOSourceNotSupported) - return reconcile.Result{}, nil - } - - var diskSize resource.Quantity - diskSize, err = ds.getPVCSize(vd, pod) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) - - if errors.Is(err, service.ErrInsufficientPVCSize) { - return reconcile.Result{}, nil - } - - return reconcile.Result{}, err - } - - source := ds.getSource(supgen, ds.statService.GetDVCRImageName(pod)) - - var nodePlacement *provisioner.NodePlacement - nodePlacement, err = getNodePlacement(ctx, ds.client, vd) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, fmt.Errorf("failed to get importer tolerations: %w", err) - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vd.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Start(ctx, diskSize, sc, source, vd, supgen, service.WithNodePlacement(nodePlacement)) - if updated, err := setPhaseConditionFromStorageError(err, vd, cb); err != nil || updated { - return reconcile.Result{}, err - } - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vd.Status.Phase = v1alpha2.DiskPending - if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vd.Status.Phase = v1alpha2.DiskPending - if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vd, corev1.EventTypeWarning, vdcondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The Registry DataSource import has completed", - ) - - vd.Status.Phase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") - - vd.Status.Progress = "100%" - vd.Status.Capacity = ds.diskService.GetCapacity(pvc) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - err = ds.diskService.CheckProvisioning(ctx, pvc) - if err != nil { - return reconcile.Result{}, setPhaseConditionFromProvisioningError(ctx, err, cb, vd, dv, ds.diskService, ds.client) - } - - vd.Status.Progress = ds.diskService.GetProgress(dv, vd.Status.Progress, service.NewScaleOption(50, 100)) - vd.Status.Capacity = ds.diskService.GetCapacity(pvc) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) - - err = ds.diskService.Protect(ctx, supgen, vd, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, ptr.Deref(pvc.Spec.StorageClassName, "")) - if err != nil { - return reconcile.Result{}, err - } - - if err = setPhaseConditionForPVCProvisioningDisk(ctx, dv, vd, pvc, sc, cb, ds.diskService); err != nil { - return reconcile.Result{}, err - } - return reconcile.Result{}, nil - } - - return reconcile.Result{RequeueAfter: time.Second}, nil + return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) + } + if pvc != nil { + ctx = logger.ToContext(ctx, log.With("pvc.name", pvc.Name, "pvc.status.phase", pvc.Status.Phase)) + } + + return steptaker.NewStepTakers[*v1alpha2.VirtualDisk]( + step.NewCleanUpImporterStep(pod, ds.importerService), + step.NewReadyStep(ds.diskService, pvc, cb), + step.NewTerminatingStep(pvc), + step.NewCreateImporterStep(pvc, pod, ds.buildEnvSettings, ds.importerService, ds.recorder, cb, "The Registry DataSource import to DVCR has started"), + step.NewWaitForDVCRImporterStep(pod, ds.statService, ds.importerService, ds.client, cb), + step.NewPVCImportFromDVCRStep(pvc, pod, ds.statService, ds.diskService, ds.client, ds.recorder, cb, "The Registry DataSource import to PVC has started"), + step.NewWaitForPVCStep(pvc, ds.client, cb), + step.NewWaitForPVCImportStep(pvc, step.DVCRPodPVCImportSource(pod, ds.statService), ds.diskService, ds.statService, service.NewScaleOption(50, 100), ds.client, cb), + ).Run(ctx, vd) } func (ds RegistryDataSource) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDisk) (bool, error) { @@ -356,37 +109,17 @@ func (ds RegistryDataSource) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDi importerRequeue, err := ds.importerService.CleanUp(ctx, supgen) if err != nil { - return false, err + return false, fmt.Errorf("clean up importer: %w", err) } diskRequeue, err := ds.diskService.CleanUp(ctx, supgen) if err != nil { - return false, err + return false, fmt.Errorf("clean up disk: %w", err) } return importerRequeue || diskRequeue, nil } -func (ds RegistryDataSource) CleanUpSupplements(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { - supgen := vdsupplements.NewGenerator(vd) - - importerRequeue, err := ds.importerService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - diskRequeue, err := ds.diskService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - if importerRequeue || diskRequeue { - return reconcile.Result{RequeueAfter: time.Second}, nil - } else { - return reconcile.Result{}, nil - } -} - func (ds RegistryDataSource) Validate(ctx context.Context, vd *v1alpha2.VirtualDisk) error { if vd.Spec.DataSource == nil || vd.Spec.DataSource.ContainerImage == nil { return errors.New("container image missed for data source") @@ -414,7 +147,7 @@ func (ds RegistryDataSource) Name() string { return registryDataSource } -func (ds RegistryDataSource) getEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *importer.Settings { +func (ds RegistryDataSource) buildEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *importer.Settings { var settings importer.Settings containerImage := &datasource.ContainerRegistry{ @@ -434,30 +167,3 @@ func (ds RegistryDataSource) getEnvSettings(vd *v1alpha2.VirtualDisk, supgen sup return &settings } - -func (ds RegistryDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *cdiv1.DataVolumeSource { - // The image was preloaded from source into dvcr. - // We can't use the same data source a second time, but we can set dvcr as the data source. - // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. - url := common.DockerRegistrySchemePrefix + dvcrSourceImageName - secretName := sup.DVCRAuthSecretForDV().Name - certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } -} - -func (ds RegistryDataSource) getPVCSize(vd *v1alpha2.VirtualDisk, pod *corev1.Pod) (resource.Quantity, error) { - // Get size from the importer Pod to detect if specified PVC size is enough. - unpackedSize, err := resource.ParseQuantity(ds.statService.GetSize(pod).UnpackedBytes) - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", ds.statService.GetSize(pod).UnpackedBytes, err) - } - - return service.GetValidatedPVCSize(vd.Spec.PersistentVolumeClaim.Size, unpackedSize) -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry_test.go new file mode 100644 index 0000000000..a82c17dc16 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry_test.go @@ -0,0 +1,343 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "errors" + "log/slog" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +var _ = Describe("RegistryDataSource", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vd *v1alpha2.VirtualDisk + sc *storagev1.StorageClass + pvc *corev1.PersistentVolumeClaim + disk *RegistryDataSourceDiskServiceMock + importerSvc *RegistryDataSourceImporterServiceMock + stat *RegistryDataSourceStatServiceMock + recorder eventrecord.EventRecorderLogger + dvcrSettings *dvcr.Settings + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(v1alpha2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(storagev1.AddToScheme(scheme)).To(Succeed()) + + recorder = &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, _, _ string) {}, + } + + dvcrSettings = &dvcr.Settings{ + RegistryURL: "dvcr.example.com", + } + + sc = &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{Name: "sc"}, + } + + vd = &v1alpha2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vd", + Generation: 1, + UID: "33333333-3333-3333-3333-333333333333", + }, + Spec: v1alpha2.VirtualDiskSpec{ + DataSource: &v1alpha2.VirtualDiskDataSource{ + Type: v1alpha2.DataSourceTypeContainerImage, + ContainerImage: &v1alpha2.VirtualDiskContainerImage{ + Image: "registry.example.com/images/slackware:15", + }, + }, + }, + Status: v1alpha2.VirtualDiskStatus{ + StorageClassName: sc.Name, + Target: v1alpha2.DiskTarget{PersistentVolumeClaim: "test-pvc"}, + }, + } + + supgen := vdsupplements.NewGenerator(vd) + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.PersistentVolumeClaim().Name, + Namespace: vd.Namespace, + }, + Spec: corev1.PersistentVolumeClaimSpec{StorageClassName: ptr.To(sc.Name)}, + Status: corev1.PersistentVolumeClaimStatus{ + Capacity: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("1Gi")}, + }, + } + + disk = &RegistryDataSourceDiskServiceMock{ + GetCapacityFunc: func(_ *corev1.PersistentVolumeClaim) string { return "1Gi" }, + GetPersistentVolumeClaimFunc: func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return pvc, nil + }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + } + + importerSvc = &RegistryDataSourceImporterServiceMock{ + GetPodFunc: func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return nil, nil }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + ProtectFunc: func(_ context.Context, _ *corev1.Pod, _ supplements.Generator) error { return nil }, + } + + stat = &RegistryDataSourceStatServiceMock{ + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { return "dvcr.example.com/cvi/vd:1" }, + GetSizeFunc: func(_ *corev1.Pod) v1alpha2.ImageStatusSize { + return v1alpha2.ImageStatusSize{UnpackedBytes: "500Mi"} + }, + GetFormatFunc: func(_ *corev1.Pod) string { return "qcow2" }, + GetDownloadSpeedFunc: func(_ types.UID, _ *corev1.Pod) *v1alpha2.StatusSpeed { return nil }, + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, prev string, _ ...service.GetProgressOption) string { + if prev == "" { + return "10%" + } + return prev + }, + CheckPodFunc: func(_ *corev1.Pod) error { return nil }, + } + }) + + newSyncer := func(c client.Client) *RegistryDataSource { + return NewRegistryDataSource(recorder, stat, importerSvc, disk, dvcrSettings, c) + } + + Context("Validate", func() { + It("rejects nil container image", func() { + vd.Spec.DataSource.ContainerImage = nil + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + err := newSyncer(cl).Validate(ctx, vd) + Expect(err).To(HaveOccurred()) + }) + + It("requires the image pull secret to exist when referenced", func() { + vd.Spec.DataSource.ContainerImage.ImagePullSecret.Name = "missing-secret" + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + err := newSyncer(cl).Validate(ctx, vd) + Expect(err).To(MatchError(ErrSecretNotFound)) + }) + + It("accepts the spec when the image pull secret is present", func() { + vd.Spec.DataSource.ContainerImage.ImagePullSecret.Name = "secret" + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: vd.Namespace}} + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + err := newSyncer(cl).Validate(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("VirtualDisk has just been created (no importer pod yet)", func() { + It("starts the importer pod and sets DiskProvisioning", func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + var started bool + importerSvc.StartFunc = func(_ context.Context, _ *importer.Settings, _ client.Object, _ supplements.Generator, _ *datasource.CABundle, _ ...service.Option) error { + started = true + return nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(started).To(BeTrue()) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + }) + + It("propagates QuotaExceeded as DiskFailed/QuotaExceeded", func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + importerSvc.StartFunc = func(_ context.Context, _ *importer.Settings, _ client.Object, _ supplements.Generator, _ *datasource.CABundle, _ ...service.Option) error { + return errors.New("exceeded quota: storage requested but limit reached") + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.QuotaExceeded, true) + }) + }) + + Context("Importer pod is running", func() { + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("reports Provisioning and protects the pod", func() { + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(importerSvc.ProtectCalls()).To(HaveLen(1)) + }) + }) + + Context("Importer pod completed, no PVC yet", func() { + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("kicks off the PVC import using a registry source", func() { + var started bool + disk.StartPVCImportFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, source *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) error { + started = true + Expect(source).ToNot(BeNil()) + Expect(source.Registry).ToNot(BeNil()) + return nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(started).To(BeTrue()) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + }) + + It("fails the disk when the source is ISO", func() { + stat.GetFormatFunc = func(_ *corev1.Pod) string { return "iso" } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.ProvisioningFailed, true) + }) + }) + + Context("PVC is created but not yet Bound", func() { + BeforeEach(func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "importer", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + importerSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + }) + + It("reports WaitForFirstConsumer for WFFC storage class", func() { + pvc.Status.Phase = corev1.ClaimPending + sc.VolumeBindingMode = ptr.To(storagev1.VolumeBindingWaitForFirstConsumer) + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskWaitForFirstConsumer)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.WaitingForFirstConsumer, true) + }) + }) + + Context("PVC is Bound and the import is complete", func() { + BeforeEach(func() { + pvc.Status.Phase = corev1.ClaimBound + pvc.Annotations = map[string]string{annotations.AnnPVCImportPhase: string(corev1.PodSucceeded)} + }) + + It("marks DiskReady", func() { + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskReady)) + ExpectCondition(vd, metav1.ConditionTrue, vdcondition.Ready, false) + ExpectStats(vd) + }) + }) + + Context("CleanUp", func() { + It("delegates to both importer and disk services", func() { + var importerCleaned, diskCleaned bool + importerSvc.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + importerCleaned = true + return false, nil + } + disk.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + diskCleaned = true + return true, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + requeue, err := newSyncer(cl).CleanUp(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(requeue).To(BeTrue()) + Expect(importerCleaned).To(BeTrue()) + Expect(diskCleaned).To(BeTrue()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go index b01c4b8579..ded4062f64 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/sources.go @@ -18,26 +18,10 @@ package source import ( "context" - "errors" "fmt" - "time" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/deckhouse/virtualization-controller/pkg/common/annotations" - "github.com/deckhouse/virtualization-controller/pkg/common/object" - "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/service/volumemode" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" - vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) @@ -84,289 +68,8 @@ func (s Sources) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDisk) (bool, e return requeue, nil } -type SupplementsCleaner interface { - CleanUpSupplements(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) -} - -func CleanUpSupplements(ctx context.Context, vd *v1alpha2.VirtualDisk, c SupplementsCleaner) (reconcile.Result, error) { - if object.ShouldCleanupSubResources(vd) { - return c.CleanUpSupplements(ctx, vd) - } - - return reconcile.Result{}, nil -} - +// IsDiskProvisioningFinished reports whether the disk has reached a terminal +// provisioning state: Ready, Lost, or Exporting. func IsDiskProvisioningFinished(c metav1.Condition) bool { return c.Reason == vdcondition.Ready.String() || c.Reason == vdcondition.Lost.String() || c.Reason == vdcondition.Exporting.String() } - -func setPhaseConditionForFinishedDisk( - pvc *corev1.PersistentVolumeClaim, - cb *conditions.ConditionBuilder, - phase *v1alpha2.DiskPhase, - supgen supplements.Generator, -) { - var newPhase v1alpha2.DiskPhase - switch { - case pvc == nil: - newPhase = v1alpha2.DiskLost - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Lost). - Message(fmt.Sprintf("PVC %s not found.", supgen.PersistentVolumeClaim().String())) - case pvc.Status.Phase == corev1.ClaimLost: - cb.Status(metav1.ConditionFalse) - if pvc.GetAnnotations()[annotations.AnnDataExportRequest] == "true" { - newPhase = v1alpha2.DiskExporting - cb.Reason(vdcondition.Exporting).Message("PV is being exported") - } else { - newPhase = v1alpha2.DiskLost - cb.Reason(vdcondition.Lost).Message(fmt.Sprintf("PV %s not found.", pvc.Spec.VolumeName)) - } - default: - newPhase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") - } - if phase != nil && string(newPhase) != "" { - *phase = newPhase - } -} - -type CheckImportProcess interface { - CheckImportProcess(ctx context.Context, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error -} - -func setPhaseConditionFromStorageError(err error, vd *v1alpha2.VirtualDisk, cb *conditions.ConditionBuilder) (bool, error) { - switch { - case err == nil: - return false, nil - case errors.Is(err, volumemode.ErrStorageProfileNotFound): - vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message("StorageProfile not found in the cluster: Please check a StorageClass name in the cluster or set a default StorageClass.") - return true, nil - case errors.Is(err, service.ErrDefaultStorageClassNotFound): - vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message("Default StorageClass not found in the cluster: please provide a StorageClass name or set a default StorageClass.") - return true, nil - default: - return false, err - } -} - -func setPhaseConditionForPVCProvisioningDisk( - ctx context.Context, - dv *cdiv1.DataVolume, - vd *v1alpha2.VirtualDisk, - pvc *corev1.PersistentVolumeClaim, - sc *storagev1.StorageClass, - cb *conditions.ConditionBuilder, - checker CheckImportProcess, -) error { - err := checker.CheckImportProcess(ctx, dv, pvc) - switch { - case err == nil: - if dv == nil { - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Waiting for the pvc importer to be created") - return nil - } - - dvRunningCond, _ := conditions.GetDataVolumeCondition(conditions.DVRunningConditionType, dv.Status.Conditions) - if isStorageClassWFFC(sc) && (dv.Status.Phase == cdiv1.PendingPopulation || dv.Status.Phase == cdiv1.WaitForFirstConsumer) && dvRunningCond.Reason == "" { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitingForFirstConsumer). - Message("The provisioning has been suspended: a created and scheduled virtual machine is awaited") - return nil - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to PVC.") - return nil - case errors.Is(err, service.ErrDataVolumeNotRunning): - vd.Status.Phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error())) - return nil - default: - return err - } -} - -func setPhaseConditionFromPodError( - ctx context.Context, - podErr error, - pod *corev1.Pod, - vd *v1alpha2.VirtualDisk, - cb *conditions.ConditionBuilder, - c client.Client, -) error { - switch { - case errors.Is(podErr, service.ErrNotInitialized): - vd.Status.Phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") - return nil - case errors.Is(podErr, service.ErrNotScheduled): - vd.Status.Phase = v1alpha2.DiskPending - - nodePlacement, err := getNodePlacement(ctx, c, vd) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return fmt.Errorf("failed to get importer tolerations: %w", err) - } - - var isChanged bool - isChanged, err = provisioner.IsNodePlacementChanged(nodePlacement, pod) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return err - } - - if isChanged { - err = c.Delete(ctx, pod) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return err - } - - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message("Provisioner recreation due to a changes in the virtual machine tolerations.") - } else { - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") - } - - return nil - case errors.Is(podErr, service.ErrProvisioningFailed): - setPhaseConditionToFailed(cb, &vd.Status.Phase, podErr) - return nil - default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", podErr)) - return podErr - } -} - -type Cleaner interface { - CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) -} - -func setPhaseConditionFromProvisioningError( - ctx context.Context, - provisioningErr error, - cb *conditions.ConditionBuilder, - vd *v1alpha2.VirtualDisk, - dv *cdiv1.DataVolume, - cleaner Cleaner, - c client.Client, -) error { - switch { - case errors.Is(provisioningErr, service.ErrDataVolumeProvisionerUnschedulable): - nodePlacement, err := getNodePlacement(ctx, c, vd) - if err != nil { - err = errors.Join(provisioningErr, err) - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) - return err - } - - isChanged, err := provisioner.IsNodePlacementChanged(nodePlacement, dv) - if err != nil { - err = errors.Join(provisioningErr, err) - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) - return err - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - - if isChanged { - supgen := vdsupplements.NewGenerator(vd) - - _, err = cleaner.CleanUp(ctx, supgen) - if err != nil { - err = errors.Join(provisioningErr, err) - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) - return err - } - - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC provisioner recreation due to a changes in the virtual machine tolerations.") - } else { - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Trying to schedule the PVC provisioner.") - } - - return nil - default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, provisioningErr) - return provisioningErr - } -} - -// Deprecated. -func getNodePlacement(ctx context.Context, c client.Client, vd *v1alpha2.VirtualDisk) (*provisioner.NodePlacement, error) { - return step.GetNodePlacement(ctx, c, vd) -} - -const retryPeriod = 1 - -func setQuotaExceededPhaseCondition(cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, err error, creationTimestamp metav1.Time) reconcile.Result { - *phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed) - - if creationTimestamp.Add(30 * time.Minute).After(time.Now()) { - cb.Message(fmt.Sprintf("Quota exceeded: %s; Please configure quotas or try recreating the resource later.", err)) - return reconcile.Result{} - } - - cb.Message(fmt.Sprintf("Quota exceeded: %s; Retry in %d minute.", err, retryPeriod)) - return reconcile.Result{RequeueAfter: retryPeriod * time.Minute} -} - -func setPhaseConditionToFailed(cb *conditions.ConditionBuilder, phase *v1alpha2.DiskPhase, err error) { - *phase = v1alpha2.DiskFailed - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error()) + ".") -} - -func isStorageClassWFFC(sc *storagev1.StorageClass) bool { - return sc != nil && sc.VolumeBindingMode != nil && *sc.VolumeBindingMode == storagev1.VolumeBindingWaitForFirstConsumer -} - -const ( - DVRunningConditionType cdiv1.DataVolumeConditionType = "Running" - DVQoutaNotExceededConditionType cdiv1.DataVolumeConditionType = "QuotaNotExceeded" - - DVImagePullFailedReason = "ImagePullFailed" -) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/clean_up_importer_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/clean_up_importer_step.go new file mode 100644 index 0000000000..1d7bee52f3 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/clean_up_importer_step.go @@ -0,0 +1,71 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type CleanUpImporterStepImporterService interface { + CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) +} + +// CleanUpImporterStep deletes the importer Pod once the disk has reached a +// final state (Ready, Lost or Exporting). It is a no-op while the disk is +// still being provisioned and when there is nothing left to clean up. +type CleanUpImporterStep struct { + pod *corev1.Pod + importer CleanUpImporterStepImporterService +} + +func NewCleanUpImporterStep( + pod *corev1.Pod, + importer CleanUpImporterStepImporterService, +) *CleanUpImporterStep { + return &CleanUpImporterStep{ + pod: pod, + importer: importer, + } +} + +func (s CleanUpImporterStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pod == nil { + return nil, nil + } + + condition, _ := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + if !isDiskProvisioningFinished(condition.Reason) { + return nil, nil + } + + supgen := vdsupplements.NewGenerator(vd) + if _, err := s.importer.CleanUp(ctx, supgen); err != nil { + return nil, fmt.Errorf("clean up importer supplements: %w", err) + } + + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/clean_up_uploader_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/clean_up_uploader_step.go new file mode 100644 index 0000000000..dc2a7bd000 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/clean_up_uploader_step.go @@ -0,0 +1,86 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type CleanUpUploaderStepUploaderService interface { + CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) +} + +// CleanUpUploaderStep deletes uploader Pod/Service/Ingress once the disk has +// reached a final state (Ready, Lost or Exporting). It is a no-op while the +// disk is still being provisioned and when there is nothing left to clean up. +type CleanUpUploaderStep struct { + pod *corev1.Pod + svc *corev1.Service + ing *netv1.Ingress + uploader CleanUpUploaderStepUploaderService +} + +func NewCleanUpUploaderStep( + pod *corev1.Pod, + svc *corev1.Service, + ing *netv1.Ingress, + uploader CleanUpUploaderStepUploaderService, +) *CleanUpUploaderStep { + return &CleanUpUploaderStep{ + pod: pod, + svc: svc, + ing: ing, + uploader: uploader, + } +} + +func (s CleanUpUploaderStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pod == nil && s.svc == nil && s.ing == nil { + return nil, nil + } + + condition, _ := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + if !isDiskProvisioningFinished(condition.Reason) { + return nil, nil + } + + supgen := vdsupplements.NewGenerator(vd) + if _, err := s.uploader.CleanUp(ctx, supgen); err != nil { + return nil, fmt.Errorf("clean up uploader supplements: %w", err) + } + + return nil, nil +} + +// isDiskProvisioningFinished reports whether the disk has reached a terminal +// provisioning state: Ready, Lost, or Exporting. +func isDiskProvisioningFinished(reason string) bool { + return reason == vdcondition.Ready.String() || + reason == vdcondition.Lost.String() || + reason == vdcondition.Exporting.String() +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_importer_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_importer_step.go new file mode 100644 index 0000000000..6562d7bc54 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_importer_step.go @@ -0,0 +1,131 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type CreateImporterStepImporterService interface { + Start(ctx context.Context, settings *importer.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error +} + +// ImporterEnvSettingsBuilder builds importer environment settings for a given +// VirtualDisk. Data sources provide their source-specific build logic via this +// callback so the step itself stays generic. +type ImporterEnvSettingsBuilder func(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *importer.Settings + +// CreateImporterStep creates an importer Pod that downloads source data into +// DVCR. It is a no-op once the underlying PVC has been created so the importer +// is not recreated after cleanup. +type CreateImporterStep struct { + pvc *corev1.PersistentVolumeClaim + pod *corev1.Pod + settingsBuilder ImporterEnvSettingsBuilder + importer CreateImporterStepImporterService + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder + eventText string +} + +func NewCreateImporterStep( + pvc *corev1.PersistentVolumeClaim, + pod *corev1.Pod, + settingsBuilder ImporterEnvSettingsBuilder, + importer CreateImporterStepImporterService, + recorder eventrecord.EventRecorderLogger, + cb *conditions.ConditionBuilder, + eventText string, +) *CreateImporterStep { + return &CreateImporterStep{ + pvc: pvc, + pod: pod, + settingsBuilder: settingsBuilder, + importer: importer, + recorder: recorder, + cb: cb, + eventText: eventText, + } +} + +func (s CreateImporterStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + // Importer is needed only until the underlying PVC has been created. + // Once the PVC exists the data has been pushed to DVCR and the importer + // supplements are not recreated even if they are missing. + if s.pvc != nil { + return nil, nil + } + + if s.pod != nil { + return nil, nil + } + + s.recorder.Event( + vd, + corev1.EventTypeNormal, + v1alpha2.ReasonDataSourceSyncStarted, + s.eventText, + ) + + vd.Status.Progress = "0%" + + supgen := vdsupplements.NewGenerator(vd) + settings := s.settingsBuilder(vd, supgen) + caBundle := datasource.NewCABundleForVMD(vd.GetNamespace(), vd.Spec.DataSource) + + err := s.importer.Start(ctx, settings, vd, supgen, caBundle, service.WithSystemNodeToleration()) + switch { + case err == nil: + // OK. + case common.ErrQuotaExceeded(err): + s.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.QuotaExceeded). + Message(fmt.Sprintf("Quota exceeded during the importer provisioning: %s", err)) + return &reconcile.Result{}, nil + default: + return nil, fmt.Errorf("start importer: %w", err) + } + + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message("DVCR Provisioner not found: create the new one.") + + return &reconcile.Result{RequeueAfter: time.Second}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_cvi_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_cvi_step.go similarity index 74% rename from images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_cvi_step.go rename to images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_cvi_step.go index da290ff1f7..0830824ef2 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_cvi_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_cvi_step.go @@ -26,7 +26,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -40,32 +39,29 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) -type CreateDataVolumeFromClusterVirtualImageStep struct { +type PVCImportFromClusterVirtualImageStep struct { pvc *corev1.PersistentVolumeClaim - dv *cdiv1.DataVolume - disk CreateDataVolumeStepDiskService + disk PVCImportStepDiskService client client.Client cb *conditions.ConditionBuilder } -func NewCreateDataVolumeFromClusterVirtualImageStep( +func NewPVCImportFromClusterVirtualImageStep( pvc *corev1.PersistentVolumeClaim, - dv *cdiv1.DataVolume, - disk CreateDataVolumeStepDiskService, + disk PVCImportStepDiskService, client client.Client, cb *conditions.ConditionBuilder, -) *CreateDataVolumeFromClusterVirtualImageStep { - return &CreateDataVolumeFromClusterVirtualImageStep{ +) *PVCImportFromClusterVirtualImageStep { + return &PVCImportFromClusterVirtualImageStep{ pvc: pvc, - dv: dv, disk: disk, client: client, cb: cb, } } -func (s CreateDataVolumeFromClusterVirtualImageStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { - if s.pvc != nil || s.dv != nil { +func (s PVCImportFromClusterVirtualImageStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pvc != nil { return nil, nil } @@ -106,10 +102,10 @@ func (s CreateDataVolumeFromClusterVirtualImageStep) Take(ctx context.Context, v return nil, err } - return NewCreateDataVolumeStep(s.dv, s.disk, s.client, source, size, s.cb).Take(ctx, vd) + return NewPVCImportStep(s.disk, s.client, source, size, s.cb).Take(ctx, vd) } -func (s CreateDataVolumeFromClusterVirtualImageStep) getPVCSize(vd *v1alpha2.VirtualDisk, cviRef *v1alpha2.ClusterVirtualImage) (resource.Quantity, error) { +func (s PVCImportFromClusterVirtualImageStep) getPVCSize(vd *v1alpha2.VirtualDisk, cviRef *v1alpha2.ClusterVirtualImage) (resource.Quantity, error) { unpackedSize, err := resource.ParseQuantity(cviRef.Status.Size.UnpackedBytes) if err != nil { return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", cviRef.Status.Size.UnpackedBytes, err) @@ -122,18 +118,16 @@ func (s CreateDataVolumeFromClusterVirtualImageStep) getPVCSize(vd *v1alpha2.Vir return service.GetValidatedPVCSize(vd.Spec.PersistentVolumeClaim.Size, unpackedSize) } -func (s CreateDataVolumeFromClusterVirtualImageStep) getSource(vd *v1alpha2.VirtualDisk, cviRef *v1alpha2.ClusterVirtualImage) *cdiv1.DataVolumeSource { +func (s PVCImportFromClusterVirtualImageStep) getSource(vd *v1alpha2.VirtualDisk, cviRef *v1alpha2.ClusterVirtualImage) *service.PVCImportSource { + return BuildClusterVirtualImagePVCImportSource(vd, cviRef) +} + +func BuildClusterVirtualImagePVCImportSource(vd *v1alpha2.VirtualDisk, cviRef *v1alpha2.ClusterVirtualImage) *service.PVCImportSource { supgen := vdsupplements.NewGenerator(vd) url := common.DockerRegistrySchemePrefix + cviRef.Status.Target.RegistryURL secretName := supgen.DVCRAuthSecretForDV().Name certConfigMapName := supgen.DVCRCABundleConfigMapForDV().Name - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_dvcr_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_dvcr_step.go new file mode 100644 index 0000000000..47afa5c34a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_dvcr_step.go @@ -0,0 +1,152 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" + podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type PVCImportFromDVCRStepStatService interface { + GetSize(pod *corev1.Pod) v1alpha2.ImageStatusSize + GetFormat(pod *corev1.Pod) string + GetDVCRImageName(pod *corev1.Pod) string + GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed +} + +// PVCImportFromDVCRStep starts the PVC import from DVCR once the helper Pod +// (uploader or importer) has finished populating DVCR. It is a no-op while the +// PVC already exists or the Pod has not yet succeeded. +type PVCImportFromDVCRStep struct { + pvc *corev1.PersistentVolumeClaim + pod *corev1.Pod + stat PVCImportFromDVCRStepStatService + disk PVCImportStepDiskService + client client.Client + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder + eventText string +} + +func NewPVCImportFromDVCRStep( + pvc *corev1.PersistentVolumeClaim, + pod *corev1.Pod, + stat PVCImportFromDVCRStepStatService, + disk PVCImportStepDiskService, + client client.Client, + recorder eventrecord.EventRecorderLogger, + cb *conditions.ConditionBuilder, + eventText string, +) *PVCImportFromDVCRStep { + return &PVCImportFromDVCRStep{ + pvc: pvc, + pod: pod, + stat: stat, + disk: disk, + client: client, + recorder: recorder, + cb: cb, + eventText: eventText, + } +} + +func (s PVCImportFromDVCRStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pvc != nil { + return nil, nil + } + + if !podutil.IsPodComplete(s.pod) { + return nil, nil + } + + s.recorder.Event( + vd, + corev1.EventTypeNormal, + v1alpha2.ReasonDataSourceSyncStarted, + s.eventText, + ) + + vd.Status.Progress = "50%" + vd.Status.DownloadSpeed = s.stat.GetDownloadSpeed(vd.GetUID(), s.pod) + + if imageformat.IsISO(s.stat.GetFormat(s.pod)) { + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(ErrISOSourceNotSupported.Error()) + ".") + return &reconcile.Result{}, nil + } + + size, err := s.getPVCSize(vd) + if err != nil { + if errors.Is(err, service.ErrInsufficientPVCSize) { + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(err.Error()) + ".") + return &reconcile.Result{}, nil + } + + return nil, err + } + + source := BuildDVCRPVCImportSource(vd, s.stat.GetDVCRImageName(s.pod)) + + return NewPVCImportStep(s.disk, s.client, source, size, s.cb).Take(ctx, vd) +} + +func (s PVCImportFromDVCRStep) getPVCSize(vd *v1alpha2.VirtualDisk) (resource.Quantity, error) { + unpackedSize, err := resource.ParseQuantity(s.stat.GetSize(s.pod).UnpackedBytes) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", s.stat.GetSize(s.pod).UnpackedBytes, err) + } + + return service.GetValidatedPVCSize(vd.Spec.PersistentVolumeClaim.Size, unpackedSize) +} + +// BuildDVCRPVCImportSource constructs a PVCImportSource for a DVCR registry +// image. The image name is typically resolved from a helper Pod that uploaded +// or downloaded the data into DVCR. +func BuildDVCRPVCImportSource(vd *v1alpha2.VirtualDisk, dvcrImageName string) *service.PVCImportSource { + supgen := vdsupplements.NewGenerator(vd) + + url := common.DockerRegistrySchemePrefix + dvcrImageName + secretName := supgen.DVCRAuthSecretForDV().Name + certConfigMapName := supgen.DVCRCABundleConfigMapForDV().Name + + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName) +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_vi_step.go similarity index 74% rename from images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go rename to images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_vi_step.go index 959f65ecfd..03f1998b0a 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_from_vi_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_from_vi_step.go @@ -26,7 +26,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -40,32 +39,29 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) -type CreateDataVolumeFromVirtualImageStep struct { +type PVCImportFromVirtualImageStep struct { pvc *corev1.PersistentVolumeClaim - dv *cdiv1.DataVolume - disk CreateDataVolumeStepDiskService + disk PVCImportStepDiskService client client.Client cb *conditions.ConditionBuilder } -func NewCreateDataVolumeFromVirtualImageStep( +func NewPVCImportFromVirtualImageStep( pvc *corev1.PersistentVolumeClaim, - dv *cdiv1.DataVolume, - disk CreateDataVolumeStepDiskService, + disk PVCImportStepDiskService, client client.Client, cb *conditions.ConditionBuilder, -) *CreateDataVolumeFromVirtualImageStep { - return &CreateDataVolumeFromVirtualImageStep{ +) *PVCImportFromVirtualImageStep { + return &PVCImportFromVirtualImageStep{ pvc: pvc, - dv: dv, disk: disk, client: client, cb: cb, } } -func (s CreateDataVolumeFromVirtualImageStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { - if s.pvc != nil || s.dv != nil { +func (s PVCImportFromVirtualImageStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pvc != nil { return nil, nil } @@ -109,10 +105,10 @@ func (s CreateDataVolumeFromVirtualImageStep) Take(ctx context.Context, vd *v1al return nil, err } - return NewCreateDataVolumeStep(s.dv, s.disk, s.client, source, size, s.cb).Take(ctx, vd) + return NewPVCImportStep(s.disk, s.client, source, size, s.cb).Take(ctx, vd) } -func (s CreateDataVolumeFromVirtualImageStep) getPVCSize(vd *v1alpha2.VirtualDisk, viRef *v1alpha2.VirtualImage) (resource.Quantity, error) { +func (s PVCImportFromVirtualImageStep) getPVCSize(vd *v1alpha2.VirtualDisk, viRef *v1alpha2.VirtualImage) (resource.Quantity, error) { unpackedSize, err := resource.ParseQuantity(viRef.Status.Size.UnpackedBytes) if err != nil { return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", viRef.Status.Size.UnpackedBytes, err) @@ -125,15 +121,14 @@ func (s CreateDataVolumeFromVirtualImageStep) getPVCSize(vd *v1alpha2.VirtualDis return service.GetValidatedPVCSize(vd.Spec.PersistentVolumeClaim.Size, unpackedSize) } -func (s CreateDataVolumeFromVirtualImageStep) getSource(vd *v1alpha2.VirtualDisk, viRef *v1alpha2.VirtualImage) (*cdiv1.DataVolumeSource, error) { +func (s PVCImportFromVirtualImageStep) getSource(vd *v1alpha2.VirtualDisk, viRef *v1alpha2.VirtualImage) (*service.PVCImportSource, error) { + return BuildVirtualImagePVCImportSource(vd, viRef) +} + +func BuildVirtualImagePVCImportSource(vd *v1alpha2.VirtualDisk, viRef *v1alpha2.VirtualImage) (*service.PVCImportSource, error) { switch viRef.Spec.Storage { case v1alpha2.StoragePersistentVolumeClaim, v1alpha2.StorageKubernetes: - return &cdiv1.DataVolumeSource{ - PVC: &cdiv1.DataVolumeSourcePVC{ - Name: viRef.Status.Target.PersistentVolumeClaim, - Namespace: viRef.Namespace, - }, - }, nil + return service.NewPVCPVCImportSource(viRef.Status.Target.PersistentVolumeClaim, viRef.Namespace), nil case v1alpha2.StorageContainerRegistry, "": supgen := vdsupplements.NewGenerator(vd) @@ -141,13 +136,7 @@ func (s CreateDataVolumeFromVirtualImageStep) getSource(vd *v1alpha2.VirtualDisk secretName := supgen.DVCRAuthSecretForDV().Name certConfigMapName := supgen.DVCRCABundleConfigMapForDV().Name - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - }, nil + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName), nil default: return nil, fmt.Errorf("unexpected virtual image storage %s, please report a bug", viRef.Spec.Storage) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_step.go similarity index 76% rename from images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_step.go rename to images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_step.go index f05746f5dc..6ce321a45c 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_dv_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_pvc_import_step.go @@ -25,7 +25,6 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -34,35 +33,30 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/service/volumemode" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" ) -type CreateDataVolumeStepDiskService interface { - Start(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *cdiv1.DataVolumeSource, obj client.Object, sup supplements.DataVolumeSupplement, opts ...service.Option) error +type PVCImportStepDiskService interface { + StartPVCImport(ctx context.Context, pvcSize resource.Quantity, sc *storagev1.StorageClass, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) error } -type CreateDataVolumeStep struct { - dv *cdiv1.DataVolume - disk CreateDataVolumeStepDiskService +type PVCImportStep struct { + disk PVCImportStepDiskService client client.Client - source *cdiv1.DataVolumeSource + source *service.PVCImportSource size resource.Quantity cb *conditions.ConditionBuilder } -func NewCreateDataVolumeStep( - dv *cdiv1.DataVolume, - disk CreateDataVolumeStepDiskService, +func NewPVCImportStep( + disk PVCImportStepDiskService, client client.Client, - source *cdiv1.DataVolumeSource, + source *service.PVCImportSource, size resource.Quantity, cb *conditions.ConditionBuilder, -) *CreateDataVolumeStep { - return &CreateDataVolumeStep{ - dv: dv, +) *PVCImportStep { + return &PVCImportStep{ disk: disk, client: client, source: source, @@ -71,15 +65,11 @@ func NewCreateDataVolumeStep( } } -func (s CreateDataVolumeStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { - if s.dv != nil { - return nil, nil +func (s PVCImportStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if vd.Status.Progress == "" { + vd.Status.Progress = "0%" } - supgen := vdsupplements.NewGenerator(vd) - - vd.Status.Progress = "0%" - sc, err := object.FetchObject(ctx, types.NamespacedName{Name: vd.Status.StorageClassName}, s.client, &storagev1.StorageClass{}) if err != nil { return nil, fmt.Errorf("get sc: %w", err) @@ -97,7 +87,7 @@ func (s CreateDataVolumeStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk return nil, fmt.Errorf("failed to get importer tolerations: %w", err) } - err = s.disk.Start(ctx, s.size, sc, s.source, vd, supgen, service.WithNodePlacement(nodePlacement)) + err = s.disk.StartPVCImport(ctx, s.size, sc, s.source, vd, nodePlacement) switch { case err == nil: // OK. diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_uploader_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_uploader_step.go new file mode 100644 index 0000000000..12c9705be5 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/create_uploader_step.go @@ -0,0 +1,144 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type CreateUploaderStepUploaderService interface { + Start(ctx context.Context, settings *uploader.Settings, obj client.Object, sup supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error +} + +type CreateUploaderStep struct { + pvc *corev1.PersistentVolumeClaim + pod *corev1.Pod + svc *corev1.Service + ing *netv1.Ingress + uploader CreateUploaderStepUploaderService + dvcrSettings *dvcr.Settings + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder +} + +func NewCreateUploaderStep( + pvc *corev1.PersistentVolumeClaim, + pod *corev1.Pod, + svc *corev1.Service, + ing *netv1.Ingress, + uploader CreateUploaderStepUploaderService, + dvcrSettings *dvcr.Settings, + recorder eventrecord.EventRecorderLogger, + cb *conditions.ConditionBuilder, +) *CreateUploaderStep { + return &CreateUploaderStep{ + pvc: pvc, + pod: pod, + svc: svc, + ing: ing, + uploader: uploader, + dvcrSettings: dvcrSettings, + recorder: recorder, + cb: cb, + } +} + +func (s CreateUploaderStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + // Uploader is needed only until the underlying PVC is created. + // Once the PVC exists the data has been uploaded to DVCR and the uploader + // supplements are not recreated even if they are missing. + if s.pvc != nil { + return nil, nil + } + + if s.pod != nil && s.svc != nil && s.ing != nil { + return nil, nil + } + + s.recorder.Event( + vd, + corev1.EventTypeNormal, + v1alpha2.ReasonDataSourceSyncStarted, + "The Upload DataSource import to DVCR has started", + ) + + vd.Status.Progress = "0%" + + supgen := vdsupplements.NewGenerator(vd) + settings := s.getEnvSettings(vd, supgen) + + err := s.uploader.Start( + ctx, settings, vd, supgen, + datasource.NewCABundleForVMD(vd.GetNamespace(), vd.Spec.DataSource), + service.WithSystemNodeToleration(), + ) + switch { + case err == nil: + // OK. + case common.ErrQuotaExceeded(err): + s.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.QuotaExceeded). + Message(fmt.Sprintf("Quota exceeded during the uploader provisioning: %s", err)) + return &reconcile.Result{}, nil + default: + return nil, fmt.Errorf("start uploader: %w", err) + } + + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message("DVCR Provisioner not found: create the new one.") + + return &reconcile.Result{RequeueAfter: time.Second}, nil +} + +func (s CreateUploaderStep) getEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *uploader.Settings { + var settings uploader.Settings + + uploader.ApplyDVCRDestinationSettings( + &settings, + s.dvcrSettings, + supgen, + s.dvcrSettings.RegistryImageForVD(vd), + ) + + return &settings +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go deleted file mode 100644 index 120505af0f..0000000000 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ensure_node_placement.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package step - -import ( - "context" - "errors" - "fmt" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" -) - -type EnsureNodePlacementStepDiskService interface { - CheckProvisioning(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error - CleanUp(ctx context.Context, sup supplements.Generator) (bool, error) -} - -// EnsureNodePlacementStep supports changing the node placement only if the PVC is created using a DataVolume. -type EnsureNodePlacementStep struct { - pvc *corev1.PersistentVolumeClaim - dv *cdiv1.DataVolume - disk EnsureNodePlacementStepDiskService - client client.Client - cb *conditions.ConditionBuilder -} - -func NewEnsureNodePlacementStep( - pvc *corev1.PersistentVolumeClaim, - dv *cdiv1.DataVolume, - disk EnsureNodePlacementStepDiskService, - client client.Client, - cb *conditions.ConditionBuilder, -) *EnsureNodePlacementStep { - return &EnsureNodePlacementStep{ - pvc: pvc, - dv: dv, - disk: disk, - client: client, - cb: cb, - } -} - -func (s EnsureNodePlacementStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { - if s.pvc == nil { - return nil, nil - } - - err := s.disk.CheckProvisioning(ctx, s.pvc) - switch { - case err == nil: - // OK. - return nil, nil - case errors.Is(err, service.ErrDataVolumeProvisionerUnschedulable): - // Will be processed below. - default: - return nil, fmt.Errorf("check provisioning: %w", err) - } - - nodePlacement, err := GetNodePlacement(ctx, s.client, vd) - if err != nil { - return nil, fmt.Errorf("get node placement: %w", err) - } - - isChanged, err := provisioner.IsNodePlacementChanged(nodePlacement, s.dv) - if err != nil { - return nil, fmt.Errorf("is node placement changed: %w", err) - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - - if !isChanged { - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Trying to schedule the PersistentVolumeClaim provisioner.") - return &reconcile.Result{}, nil - } - - supgen := vdsupplements.NewGenerator(vd) - - _, err = s.disk.CleanUp(ctx, supgen) - if err != nil { - return nil, fmt.Errorf("clean up due to changes in the virtual machine tolerations: %w", err) - } - - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("The PersistentVolumeClaim provisioner will be recreated due to changes in the virtual machine tolerations.") - return &reconcile.Result{}, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ready_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ready_step.go index 1889ced6ce..1ea1d36c85 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ready_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/ready_step.go @@ -22,8 +22,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/annotations" @@ -41,7 +39,6 @@ const readyStep = "ready" type ReadyStepDiskService interface { GetCapacity(pvc *corev1.PersistentVolumeClaim) string CleanUpSupplements(ctx context.Context, sup supplements.Generator) (bool, error) - Protect(ctx context.Context, sup supplements.Generator, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error } type ReadyStep struct { @@ -80,6 +77,10 @@ func (s ReadyStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconci } vdsupplements.SetPVCName(vd, s.pvc.Name) + if phase := s.pvc.GetAnnotations()[annotations.AnnPVCImportPhase]; phase != "" && phase != string(corev1.PodSucceeded) { + log.Debug("PVC import is not completed yet") + return nil, nil + } switch s.pvc.Status.Phase { case corev1.ClaimLost: @@ -108,15 +109,9 @@ func (s ReadyStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconci log.Debug("PVC is Bound") - supgen := vdsupplements.NewGenerator(vd) - err := s.diskService.Protect(ctx, supgen, vd, nil, s.pvc) - if err != nil { - return nil, fmt.Errorf("protect underlying pvc: %w", err) - } - if object.ShouldCleanupSubResources(vd) { - _, err = s.diskService.CleanUpSupplements(ctx, supgen) - if err != nil { + supgen := vdsupplements.NewGenerator(vd) + if _, err := s.diskService.CleanUpSupplements(ctx, supgen); err != nil { return nil, fmt.Errorf("clean up supplements: %w", err) } } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go deleted file mode 100644 index 667be675e0..0000000000 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dv_step.go +++ /dev/null @@ -1,230 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package step - -import ( - "context" - "fmt" - "strings" - - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - dvutil "github.com/deckhouse/virtualization-controller/pkg/common/datavolume" - "github.com/deckhouse/virtualization-controller/pkg/common/object" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" - "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" -) - -type WaitForDVStepDiskService interface { - GetProgress(dv *cdiv1.DataVolume, prevProgress string, opts ...service.GetProgressOption) string -} - -type WaitForDVStep struct { - pvc *corev1.PersistentVolumeClaim - dv *cdiv1.DataVolume - disk WaitForDVStepDiskService - client client.Client - cb *conditions.ConditionBuilder -} - -func NewWaitForDVStep( - pvc *corev1.PersistentVolumeClaim, - dv *cdiv1.DataVolume, - disk WaitForDVStepDiskService, - client client.Client, - cb *conditions.ConditionBuilder, -) *WaitForDVStep { - return &WaitForDVStep{ - pvc: pvc, - dv: dv, - disk: disk, - client: client, - cb: cb, - } -} - -func (s WaitForDVStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { - if s.dv == nil { - vd.Status.Phase = v1alpha2.DiskProvisioning - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Waiting for the VirtualDisk importer to be created.") - return &reconcile.Result{}, nil - } - - vd.Status.Progress = s.disk.GetProgress(s.dv, vd.Status.Progress, service.NewScaleOption(0, 100)) - vdsupplements.SetPVCName(vd, s.dv.Status.ClaimName) - - set, err := s.setForFirstConsumerIsAwaited(ctx, vd) - if err != nil { - return nil, fmt.Errorf("set for first consumer is awaited: %w", err) - } - ok := s.checkQoutaNotExceededCondition(vd, set) - if !ok { - return &reconcile.Result{}, nil - } - if set { - return &reconcile.Result{}, nil - } - - ok = s.checkRunningCondition(vd) - if !ok { - return &reconcile.Result{}, nil - } - - ok, err = s.checkImporterPrimePod(ctx, vd) - if err != nil { - return nil, fmt.Errorf("check importer prime pod: %w", err) - } - if !ok { - return &reconcile.Result{}, nil - } - - set = s.setForProvisioning(vd) - if set { - return &reconcile.Result{}, nil - } - - return nil, nil -} - -func (s WaitForDVStep) setForProvisioning(vd *v1alpha2.VirtualDisk) (set bool) { - if s.dv.Status.Phase != cdiv1.Succeeded { - vd.Status.Phase = v1alpha2.DiskProvisioning - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to the PersistentVolumeClaim.") - return true - } - - return false -} - -func (s WaitForDVStep) setForFirstConsumerIsAwaited(ctx context.Context, vd *v1alpha2.VirtualDisk) (set bool, err error) { - if vd.Status.StorageClassName == "" { - return false, fmt.Errorf("StorageClassName is empty, please report a bug") - } - - sc, err := object.FetchObject(ctx, types.NamespacedName{Name: vd.Status.StorageClassName}, s.client, &storagev1.StorageClass{}) - if err != nil { - return false, fmt.Errorf("get sc: %w", err) - } - - isWFFC := sc != nil && sc.VolumeBindingMode != nil && *sc.VolumeBindingMode == storagev1.VolumeBindingWaitForFirstConsumer - dvRunningCond, _ := conditions.GetDataVolumeCondition(conditions.DVRunningConditionType, s.dv.Status.Conditions) - dvRunningReasonEmptyOrPending := dvRunningCond.Reason == "" || dvRunningCond.Reason == conditions.DVRunningConditionPendingReason - if isWFFC && (s.dv.Status.Phase == cdiv1.PendingPopulation || s.dv.Status.Phase == cdiv1.WaitForFirstConsumer) && dvRunningReasonEmptyOrPending { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitingForFirstConsumer). - Message("The provisioning has been suspended: a created and scheduled virtual machine is awaited.") - return true, nil - } - - return false, nil -} - -func (s WaitForDVStep) checkQoutaNotExceededCondition(vd *v1alpha2.VirtualDisk, inwffc bool) (ok bool) { - dvQuotaNotExceededCondition, _ := conditions.GetDataVolumeCondition(conditions.DVQoutaNotExceededConditionType, s.dv.Status.Conditions) - if dvQuotaNotExceededCondition.Status == corev1.ConditionFalse { - vd.Status.Phase = v1alpha2.DiskPending - if inwffc { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return false - } - - return true -} - -func (s WaitForDVStep) checkRunningCondition(vd *v1alpha2.VirtualDisk) (ok bool) { - dvRunningCondition, _ := conditions.GetDataVolumeCondition(conditions.DVRunningConditionType, s.dv.Status.Conditions) - switch { - case dvRunningCondition.Reason == conditions.DVImagePullFailedReason: - vd.Status.Phase = v1alpha2.DiskFailed - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) - return false - case strings.Contains(dvRunningCondition.Reason, "Error"): - vd.Status.Phase = v1alpha2.DiskFailed - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(dvRunningCondition.Message) - return false - default: - return true - } -} - -func (s WaitForDVStep) checkImporterPrimePod(ctx context.Context, vd *v1alpha2.VirtualDisk) (ok bool, err error) { - if s.pvc == nil { - return true, nil - } - - cdiImporterPrimeKey := types.NamespacedName{ - Namespace: s.pvc.Namespace, - Name: dvutil.GetImporterPrimeName(s.pvc.UID), - } - - cdiImporterPrime, err := object.FetchObject(ctx, cdiImporterPrimeKey, s.client, &corev1.Pod{}) - if err != nil { - return false, fmt.Errorf("fetch importer prime pod: %w", err) - } - - if cdiImporterPrime != nil { - podInitializedCond, _ := conditions.GetPodCondition(corev1.PodInitialized, cdiImporterPrime.Status.Conditions) - if podInitializedCond.Status == corev1.ConditionFalse && strings.Contains(podInitializedCond.Reason, "Error") { - vd.Status.Phase = v1alpha2.DiskPending - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(fmt.Sprintf("The PVC importer is not initialized; %s error %s: %s", cdiImporterPrimeKey.String(), podInitializedCond.Reason, podInitializedCond.Message)) - return false, nil - } - - podScheduledCond, _ := conditions.GetPodCondition(corev1.PodScheduled, cdiImporterPrime.Status.Conditions) - if podScheduledCond.Status == corev1.ConditionFalse && strings.Contains(podScheduledCond.Reason, "Error") { - vd.Status.Phase = v1alpha2.DiskPending - s.cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(fmt.Sprintf("The PVC importer is not scheduled; %s error %s: %s", cdiImporterPrimeKey.String(), podScheduledCond.Reason, podScheduledCond.Message)) - return false, nil - } - } - - return true, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dvcr_importer_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dvcr_importer_step.go new file mode 100644 index 0000000000..357ab5e1cd --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_dvcr_importer_step.go @@ -0,0 +1,158 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "errors" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type WaitForDVCRImporterStepStatService interface { + CheckPod(pod *corev1.Pod) error + GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed +} + +type WaitForDVCRImporterStepImporterService interface { + Protect(ctx context.Context, pod *corev1.Pod, sup supplements.Generator) error +} + +// WaitForDVCRImporterStep tracks an importer Pod that downloads source data into +// DVCR. It is a no-op while the Pod is missing or has already completed, in +// which case downstream steps take over. +type WaitForDVCRImporterStep struct { + pod *corev1.Pod + stat WaitForDVCRImporterStepStatService + importer WaitForDVCRImporterStepImporterService + client client.Client + cb *conditions.ConditionBuilder +} + +func NewWaitForDVCRImporterStep( + pod *corev1.Pod, + stat WaitForDVCRImporterStepStatService, + importer WaitForDVCRImporterStepImporterService, + client client.Client, + cb *conditions.ConditionBuilder, +) *WaitForDVCRImporterStep { + return &WaitForDVCRImporterStep{ + pod: pod, + stat: stat, + importer: importer, + client: client, + cb: cb, + } +} + +func (s WaitForDVCRImporterStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pod == nil || podutil.IsPodComplete(s.pod) { + return nil, nil + } + + if err := s.stat.CheckPod(s.pod); err != nil { + return s.handlePodError(ctx, vd, err) + } + + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message("Import is in the process of provisioning to DVCR.") + + vd.Status.Progress = s.stat.GetProgress(vd.GetUID(), s.pod, vd.Status.Progress, service.NewScaleOption(0, 50)) + vd.Status.DownloadSpeed = s.stat.GetDownloadSpeed(vd.GetUID(), s.pod) + + supgen := vdsupplements.NewGenerator(vd) + if err := s.importer.Protect(ctx, s.pod, supgen); err != nil { + return nil, fmt.Errorf("protect importer pod: %w", err) + } + + return &reconcile.Result{RequeueAfter: 2 * time.Second}, nil +} + +func (s WaitForDVCRImporterStep) handlePodError(ctx context.Context, vd *v1alpha2.VirtualDisk, podErr error) (*reconcile.Result, error) { + switch { + case errors.Is(podErr, service.ErrNotInitialized): + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningNotStarted). + Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + return &reconcile.Result{}, nil + case errors.Is(podErr, service.ErrNotScheduled): + vd.Status.Phase = v1alpha2.DiskPending + + nodePlacement, err := GetNodePlacement(ctx, s.client, vd) + if err != nil { + return nil, fmt.Errorf("get node placement: %w", err) + } + + isChanged, err := provisioner.IsNodePlacementChanged(nodePlacement, s.pod) + if err != nil { + return nil, fmt.Errorf("check node placement: %w", err) + } + + if isChanged { + if err := s.client.Delete(ctx, s.pod); err != nil { + return nil, fmt.Errorf("recreate importer pod: %w", err) + } + + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningNotStarted). + Message("Provisioner recreation due to a changes in the virtual machine tolerations.") + } else { + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningNotStarted). + Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + } + + return &reconcile.Result{}, nil + case errors.Is(podErr, service.ErrProvisioningFailed): + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + return &reconcile.Result{}, nil + default: + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(fmt.Errorf("unexpected error: %w", podErr).Error()) + ".") + return nil, podErr + } +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_import_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_import_step.go new file mode 100644 index 0000000000..7006f13d6b --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_import_step.go @@ -0,0 +1,190 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +// pvcImportProgressRequeue is how often the step refreshes vd.Status.Progress +// from the cdi-importer pod metrics while the import is in flight. +const pvcImportProgressRequeue = 2 * time.Second + +type WaitForPVCImportStepDiskService interface { + EnsurePVCImport(ctx context.Context, target *corev1.PersistentVolumeClaim, source *service.PVCImportSource, vd *v1alpha2.VirtualDisk, nodePlacement *provisioner.NodePlacement) (corev1.PodPhase, error) +} + +// WaitForPVCImportStepStatService is the subset of StatService used to extract +// the cdi-importer pod's progress and project it into vd.Status.Progress. +type WaitForPVCImportStepStatService interface { + GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string +} + +// PVCImportSourceProvider builds the PVCImportSource used by WaitForPVCImportStep. +// It is invoked lazily so the source can be derived from objects (Pod, CVI, VI) +// inside the step instead of being constructed by the data source up front. +type PVCImportSourceProvider func(ctx context.Context, vd *v1alpha2.VirtualDisk) (*service.PVCImportSource, error) + +// WaitForPVCImportStep drives the import of data from DVCR (or another +// PVCImportSource) into the target PersistentVolumeClaim and reflects its +// progress in the VirtualDisk status. It is a no-op until the PVC has been +// created and reached the Bound phase. +// +// While the import is running the step requeues every pvcImportProgressRequeue +// and republishes vd.Status.Progress from the cdi-importer pod's +// kubevirt_cdi_import_progress_total metric (0..100). When progressScale is +// set, the value is projected into the [progressScale.Low, progressScale.High] +// slice of the disk-wide progress (e.g. 50..100 for HTTP / Registry / Upload +// data sources where the first 50% is already filled by the DVCR phase). +type WaitForPVCImportStep struct { + pvc *corev1.PersistentVolumeClaim + sourceProvider PVCImportSourceProvider + disk WaitForPVCImportStepDiskService + stat WaitForPVCImportStepStatService + progressScale *service.ScaleOption + client client.Client + cb *conditions.ConditionBuilder +} + +func NewWaitForPVCImportStep( + pvc *corev1.PersistentVolumeClaim, + sourceProvider PVCImportSourceProvider, + disk WaitForPVCImportStepDiskService, + stat WaitForPVCImportStepStatService, + progressScale *service.ScaleOption, + client client.Client, + cb *conditions.ConditionBuilder, +) *WaitForPVCImportStep { + return &WaitForPVCImportStep{ + pvc: pvc, + sourceProvider: sourceProvider, + disk: disk, + stat: stat, + progressScale: progressScale, + client: client, + cb: cb, + } +} + +func (s WaitForPVCImportStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pvc == nil || s.pvc.Status.Phase != corev1.ClaimBound { + return nil, nil + } + + nodePlacement, err := GetNodePlacement(ctx, s.client, vd) + if err != nil { + return nil, fmt.Errorf("failed to get importer tolerations: %w", err) + } + + var source *service.PVCImportSource + if s.sourceProvider != nil { + source, err = s.sourceProvider(ctx, vd) + if err != nil { + return nil, fmt.Errorf("build pvc import source: %w", err) + } + } + + phase, err := s.disk.EnsurePVCImport(ctx, s.pvc, source, vd, nodePlacement) + if err != nil { + return nil, fmt.Errorf("ensure pvc import: %w", err) + } + + switch phase { + case corev1.PodSucceeded: + return &reconcile.Result{RequeueAfter: time.Second}, nil + case corev1.PodFailed: + vd.Status.Phase = v1alpha2.DiskFailed + s.cb.Status(metav1.ConditionFalse).Reason(vdcondition.ProvisioningFailed).Message("VirtualDisk importer Pod failed.") + return &reconcile.Result{}, nil + default: + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb.Status(metav1.ConditionFalse).Reason(vdcondition.Provisioning).Message("Import is in the process of provisioning to the PersistentVolumeClaim.") + + if err := s.refreshProgressFromPod(ctx, vd); err != nil { + return nil, err + } + + return &reconcile.Result{RequeueAfter: pvcImportProgressRequeue}, nil + } +} + +// refreshProgressFromPod queries the cdi-importer pod (named after the target +// PVC) for its progress metric and updates vd.Status.Progress. Silently keeps +// the previous value when stat/pod is missing or metrics are not yet readable. +func (s WaitForPVCImportStep) refreshProgressFromPod(ctx context.Context, vd *v1alpha2.VirtualDisk) error { + if s.stat == nil { + return nil + } + + pod, err := object.FetchObject(ctx, types.NamespacedName{Name: s.pvc.Name, Namespace: s.pvc.Namespace}, s.client, &corev1.Pod{}) + if err != nil { + return fmt.Errorf("fetch cdi-importer pod: %w", err) + } + if pod == nil { + return nil + } + + var opts []service.GetProgressOption + if s.progressScale != nil { + opts = append(opts, s.progressScale) + } + vd.Status.Progress = s.stat.GetProgress(vd.GetUID(), pod, vd.Status.Progress, opts...) + return nil +} + +// StaticPVCImportSource returns a PVCImportSourceProvider that always returns +// the given source. It is useful when the source is fully known up front, e.g. +// for ObjectRef-based data sources. +func StaticPVCImportSource(source *service.PVCImportSource) PVCImportSourceProvider { + return func(_ context.Context, _ *v1alpha2.VirtualDisk) (*service.PVCImportSource, error) { + return source, nil + } +} + +// DVCRPodPVCImportSource returns a PVCImportSourceProvider that builds a +// registry-backed PVCImportSource using the DVCR image name resolved from the +// provided helper Pod (uploader or importer). +func DVCRPodPVCImportSource(pod *corev1.Pod, stat interface { + GetDVCRImageName(pod *corev1.Pod) string +}, +) PVCImportSourceProvider { + return func(_ context.Context, vd *v1alpha2.VirtualDisk) (*service.PVCImportSource, error) { + if pod == nil { + return nil, nil + } + dvcrImageName := stat.GetDVCRImageName(pod) + if dvcrImageName == "" { + return nil, nil + } + return BuildDVCRPVCImportSource(vd, dvcrImageName), nil + } +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_step.go index 538b7f1e15..2382638cfc 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_step.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_pvc_step.go @@ -52,6 +52,10 @@ func NewWaitForPVCStep( } func (s WaitForPVCStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if vd.Status.Progress == "" { + vd.Status.Progress = "0%" + } + if s.pvc == nil { vd.Status.Phase = v1alpha2.DiskProvisioning s.cb. diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_user_upload_step.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_user_upload_step.go new file mode 100644 index 0000000000..ae73d374ca --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/step/wait_for_user_upload_step.go @@ -0,0 +1,187 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package step + +import ( + "context" + "errors" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type WaitForUserUploadStepStatService interface { + CheckPod(pod *corev1.Pod) error + IsUploadStarted(ownerUID types.UID, pod *corev1.Pod) bool + IsUploaderReady(pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress, tlsSecret *corev1.Secret) (bool, error) + GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *v1alpha2.StatusSpeed +} + +type WaitForUserUploadStepUploaderService interface { + GetExternalURL(ctx context.Context, ing *netv1.Ingress) string + GetInClusterURL(ctx context.Context, svc *corev1.Service) string + Protect(ctx context.Context, sup supplements.Generator, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error +} + +type WaitForUserUploadStep struct { + pod *corev1.Pod + svc *corev1.Service + ing *netv1.Ingress + stat WaitForUserUploadStepStatService + uploader WaitForUserUploadStepUploaderService + client client.Client + cb *conditions.ConditionBuilder +} + +func NewWaitForUserUploadStep( + pod *corev1.Pod, + svc *corev1.Service, + ing *netv1.Ingress, + stat WaitForUserUploadStepStatService, + uploader WaitForUserUploadStepUploaderService, + client client.Client, + cb *conditions.ConditionBuilder, +) *WaitForUserUploadStep { + return &WaitForUserUploadStep{ + pod: pod, + svc: svc, + ing: ing, + stat: stat, + uploader: uploader, + client: client, + cb: cb, + } +} + +func (s WaitForUserUploadStep) Take(ctx context.Context, vd *v1alpha2.VirtualDisk) (*reconcile.Result, error) { + if s.pod == nil || podutil.IsPodComplete(s.pod) { + return nil, nil + } + + supgen := vdsupplements.NewGenerator(vd) + uploadStarted := s.stat.IsUploadStarted(vd.GetUID(), s.pod) || hasUploadProgress(vd.Status.Progress) + + if err := s.stat.CheckPod(s.pod); err != nil { + return s.handlePodError(ctx, vd, err, uploadStarted) + } + + if !uploadStarted { + tlsSecret, err := supplements.GetTLSSecret(ctx, s.client, supgen.Generator) + if err != nil { + return nil, fmt.Errorf("fetch uploader tls secret: %w", err) + } + + isUploaderReady, err := s.stat.IsUploaderReady(s.pod, s.svc, s.ing, tlsSecret) + if err != nil { + return nil, fmt.Errorf("check uploader readiness: %w", err) + } + + if isUploaderReady { + vd.Status.Phase = v1alpha2.DiskWaitForUserUpload + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.WaitForUserUpload). + Message("Waiting for the user upload.") + vd.Status.ImageUploadURLs = &v1alpha2.ImageUploadURLs{ + External: s.uploader.GetExternalURL(ctx, s.ing), + InCluster: s.uploader.GetInClusterURL(ctx, s.svc), + } + } else { + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message(fmt.Sprintf("Waiting for the uploader %q to be ready to process the user's upload.", s.pod.Name)) + } + + return &reconcile.Result{RequeueAfter: time.Second}, nil + } + + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message("Import is in the process of provisioning to DVCR.") + + vd.Status.Progress = s.stat.GetProgress(vd.GetUID(), s.pod, vd.Status.Progress, service.NewScaleOption(0, 50)) + vd.Status.DownloadSpeed = s.stat.GetDownloadSpeed(vd.GetUID(), s.pod) + + if err := s.uploader.Protect(ctx, supgen, s.pod, s.svc, s.ing); err != nil { + return nil, fmt.Errorf("protect uploader supplements: %w", err) + } + + return &reconcile.Result{RequeueAfter: time.Second}, nil +} + +func (s WaitForUserUploadStep) handlePodError(_ context.Context, vd *v1alpha2.VirtualDisk, podErr error, uploadStarted bool) (*reconcile.Result, error) { + switch { + case errors.Is(podErr, service.ErrNotInitialized), errors.Is(podErr, service.ErrNotScheduled): + if uploadStarted { + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message("Import is in the process of provisioning to DVCR.") + return &reconcile.Result{RequeueAfter: time.Second}, nil + } + + vd.Status.Phase = v1alpha2.DiskProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + return &reconcile.Result{}, nil + case errors.Is(podErr, service.ErrProvisioningFailed): + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(podErr.Error()) + ".") + return &reconcile.Result{}, nil + default: + vd.Status.Phase = v1alpha2.DiskFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(fmt.Errorf("unexpected error: %w", podErr).Error()) + ".") + return nil, podErr + } +} + +func hasUploadProgress(progress string) bool { + switch progress { + case "", "0", "0%", "0.0%", "0.00%": + return false + default: + return true + } +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go index 62bd37e8c2..771315128e 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go @@ -18,29 +18,15 @@ package source import ( "context" - "errors" "fmt" - "time" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/deckhouse/virtualization-controller/pkg/common" - "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/common/imageformat" - "github.com/deckhouse/virtualization-controller/pkg/common/object" - podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" - "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/common/steptaker" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/source/step" vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" @@ -52,9 +38,9 @@ import ( const uploadDataSource = "upload" type UploadDataSource struct { - statService *service.StatService - uploaderService *service.UploaderService - diskService *service.DiskService + statService UploadDataSourceStatService + uploaderService UploadDataSourceUploaderService + diskService UploadDataSourceDiskService dvcrSettings *dvcr.Settings recorder eventrecord.EventRecorderLogger client client.Client @@ -62,9 +48,9 @@ type UploadDataSource struct { func NewUploadDataSource( recorder eventrecord.EventRecorderLogger, - statService *service.StatService, - uploaderService *service.UploaderService, - diskService *service.DiskService, + statService UploadDataSourceStatService, + uploaderService UploadDataSourceUploaderService, + diskService UploadDataSourceDiskService, dvcrSettings *dvcr.Settings, client client.Client, ) *UploadDataSource { @@ -81,322 +67,42 @@ func NewUploadDataSource( func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { log, ctx := logger.GetDataSourceContext(ctx, uploadDataSource) - condition, _ := conditions.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + supgen := vdsupplements.NewGenerator(vd) + cb := conditions.NewConditionBuilder(vdcondition.ReadyType).Generation(vd.Generation) defer func() { conditions.SetCondition(cb, &vd.Status.Conditions) }() - supgen := vdsupplements.NewGenerator(vd) - pod, err := ds.uploaderService.GetPod(ctx, supgen) if err != nil { - return reconcile.Result{}, err + return reconcile.Result{}, fmt.Errorf("fetch uploader pod: %w", err) } svc, err := ds.uploaderService.GetService(ctx, supgen) if err != nil { - return reconcile.Result{}, err + return reconcile.Result{}, fmt.Errorf("fetch uploader service: %w", err) } ing, err := ds.uploaderService.GetIngress(ctx, supgen) if err != nil { - return reconcile.Result{}, err - } - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vd.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) - } - - tlsSecret, err := supplements.GetTLSSecret(ctx, ds.client, supgen.Generator) - if err != nil { - return reconcile.Result{}, err + return reconcile.Result{}, fmt.Errorf("fetch uploader ingress: %w", err) } - isUploaderReady, err := ds.statService.IsUploaderReady(pod, svc, ing, tlsSecret) + pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { - return reconcile.Result{}, err - } - - switch { - case IsDiskProvisioningFinished(condition): - log.Debug("Disk provisioning finished: clean up") - - setPhaseConditionForFinishedDisk(pvc, cb, &vd.Status.Phase, supgen) - - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen.Generator, vd, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - // Unprotect upload time supplements to delete them later. - err = ds.uploaderService.Unprotect(ctx, supgen, pod, svc, ing) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, supgen, dv) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vd, ds) - case object.AnyTerminating(pod, svc, ing, dv, pvc): - log.Info("Waiting for supplements to be terminated") - case pod == nil || svc == nil || ing == nil: - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncStarted, - "The Upload DataSource import to DVCR has started", - ) - - vd.Status.Progress = "0%" - - envSettings := ds.getEnvSettings(vd, supgen.Generator) - - err = ds.uploaderService.Start( - ctx, envSettings, vd, supgen, - datasource.NewCABundleForVMD(vd.GetNamespace(), vd.Spec.DataSource), - service.WithSystemNodeToleration(), - ) - switch { - case err == nil: - // OK. - case common.ErrQuotaExceeded(err): - ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vd.Status.Phase, err, vd.CreationTimestamp), nil - default: - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, err - } - - vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitForUserUpload). - Message("DVCR Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case !podutil.IsPodComplete(pod): - err = ds.statService.CheckPod(pod) - if err != nil { - return reconcile.Result{}, setPhaseConditionFromPodError(ctx, err, pod, vd, cb, ds.client) - } - - if !ds.statService.IsUploadStarted(vd.GetUID(), pod) { - if isUploaderReady { - log.Info("Waiting for the user upload", "pod.phase", pod.Status.Phase) - - vd.Status.Phase = v1alpha2.DiskWaitForUserUpload - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.WaitForUserUpload). - Message("Waiting for the user upload.") - vd.Status.ImageUploadURLs = &v1alpha2.ImageUploadURLs{ - External: ds.uploaderService.GetExternalURL(ctx, ing), - InCluster: ds.uploaderService.GetInClusterURL(ctx, svc), - } - } else { - log.Info("Waiting for the uploader to be ready to process the user's upload", "pod.phase", pod.Status.Phase) - - vd.Status.Phase = v1alpha2.DiskPending - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningNotStarted). - Message(fmt.Sprintf("Waiting for the uploader %q to be ready to process the user's upload.", pod.Name)) - } - - return reconcile.Result{RequeueAfter: time.Second}, nil - } - - log.Info("Provisioning to DVCR is in progress", "podPhase", pod.Status.Phase) - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("Import is in the process of provisioning to DVCR.") - - vd.Status.Progress = ds.statService.GetProgress(vd.GetUID(), pod, vd.Status.Progress, service.NewScaleOption(0, 50)) - vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) - - err = ds.uploaderService.Protect(ctx, supgen, pod, svc, ing) - if err != nil { - return reconcile.Result{}, err - } - case dv == nil: - if isStorageClassWFFC(sc) && len(vd.Status.AttachedToVirtualMachines) != 1 { - vd.Status.Progress = "50%" - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - return reconcile.Result{}, nil - } - - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncStarted, - "The Upload DataSource import to PVC has started", - ) - - err = ds.statService.CheckPod(pod) - if err != nil { - vd.Status.Phase = v1alpha2.DiskFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - vd.Status.Progress = "50%" - vd.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vd.GetUID(), pod) - - if imageformat.IsISO(ds.statService.GetFormat(pod)) { - setPhaseConditionToFailed(cb, &vd.Status.Phase, ErrISOSourceNotSupported) - return reconcile.Result{}, nil - } - - var diskSize resource.Quantity - diskSize, err = ds.getPVCSize(vd, pod) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, err) - - if errors.Is(err, service.ErrInsufficientPVCSize) { - return reconcile.Result{}, nil - } - - return reconcile.Result{}, err - } - - source := ds.getSource(supgen.Generator, ds.statService.GetDVCRImageName(pod)) - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vd.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - - var nodePlacement *provisioner.NodePlacement - nodePlacement, err = getNodePlacement(ctx, ds.client, vd) - if err != nil { - setPhaseConditionToFailed(cb, &vd.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, fmt.Errorf("failed to get importer tolerations: %w", err) - } - - err = ds.diskService.Start(ctx, diskSize, sc, source, vd, supgen, service.WithNodePlacement(nodePlacement)) - if updated, err := setPhaseConditionFromStorageError(err, vd, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vd.Status.Phase = v1alpha2.DiskPending - if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vd.Status.Phase = v1alpha2.DiskPending - if dv.Status.ClaimName != "" && isStorageClassWFFC(sc) { - vd.Status.Phase = v1alpha2.DiskWaitForFirstConsumer - } - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vd, corev1.EventTypeWarning, vdcondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vd.Status.Phase = v1alpha2.DiskProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vdcondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - ds.recorder.Event( - vd, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The Upload DataSource import has completed", - ) - - vd.Status.Phase = v1alpha2.DiskReady - cb. - Status(metav1.ConditionTrue). - Reason(vdcondition.Ready). - Message("") - - vd.Status.Progress = "100%" - vd.Status.Capacity = ds.diskService.GetCapacity(pvc) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) - - log.Info("Ready", "vd", vd.Name, "progress", vd.Status.Progress, "dv.phase", dv.Status.Phase) - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - err = ds.diskService.CheckProvisioning(ctx, pvc) - if err != nil { - return reconcile.Result{}, setPhaseConditionFromProvisioningError(ctx, err, cb, vd, dv, ds.diskService, ds.client) - } - - vd.Status.Progress = ds.diskService.GetProgress(dv, vd.Status.Progress, service.NewScaleOption(50, 100)) - vd.Status.Capacity = ds.diskService.GetCapacity(pvc) - vdsupplements.SetPVCName(vd, dv.Status.ClaimName) - - err = ds.diskService.Protect(ctx, supgen.Generator, vd, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, ptr.Deref(pvc.Spec.StorageClassName, "")) - if err != nil { - return reconcile.Result{}, err - } - - if err = setPhaseConditionForPVCProvisioningDisk(ctx, dv, vd, pvc, sc, cb, ds.diskService); err != nil { - return reconcile.Result{}, err - } - return reconcile.Result{}, nil - } - - return reconcile.Result{RequeueAfter: time.Second}, nil + return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) + } + if pvc != nil { + ctx = logger.ToContext(ctx, log.With("pvc.name", pvc.Name, "pvc.status.phase", pvc.Status.Phase)) + } + + return steptaker.NewStepTakers[*v1alpha2.VirtualDisk]( + step.NewCleanUpUploaderStep(pod, svc, ing, ds.uploaderService), + step.NewReadyStep(ds.diskService, pvc, cb), + step.NewTerminatingStep(pvc), + step.NewCreateUploaderStep(pvc, pod, svc, ing, ds.uploaderService, ds.dvcrSettings, ds.recorder, cb), + step.NewWaitForUserUploadStep(pod, svc, ing, ds.statService, ds.uploaderService, ds.client, cb), + step.NewPVCImportFromDVCRStep(pvc, pod, ds.statService, ds.diskService, ds.client, ds.recorder, cb, "The Upload DataSource import to PVC has started"), + step.NewWaitForPVCStep(pvc, ds.client, cb), + step.NewWaitForPVCImportStep(pvc, step.DVCRPodPVCImportSource(pod, ds.statService), ds.diskService, ds.statService, service.NewScaleOption(50, 100), ds.client, cb), + ).Run(ctx, vd) } func (ds UploadDataSource) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDisk) (bool, error) { @@ -404,37 +110,17 @@ func (ds UploadDataSource) CleanUp(ctx context.Context, vd *v1alpha2.VirtualDisk uploaderRequeue, err := ds.uploaderService.CleanUp(ctx, supgen) if err != nil { - return false, err + return false, fmt.Errorf("clean up uploader: %w", err) } diskRequeue, err := ds.diskService.CleanUp(ctx, supgen) if err != nil { - return false, err + return false, fmt.Errorf("clean up disk: %w", err) } return uploaderRequeue || diskRequeue, nil } -func (ds UploadDataSource) CleanUpSupplements(ctx context.Context, vd *v1alpha2.VirtualDisk) (reconcile.Result, error) { - supgen := vdsupplements.NewGenerator(vd) - - uploaderRequeue, err := ds.uploaderService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - diskRequeue, err := ds.diskService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - if uploaderRequeue || diskRequeue { - return reconcile.Result{RequeueAfter: time.Second}, nil - } else { - return reconcile.Result{}, nil - } -} - func (ds UploadDataSource) Validate(_ context.Context, _ *v1alpha2.VirtualDisk) error { return nil } @@ -442,43 +128,3 @@ func (ds UploadDataSource) Validate(_ context.Context, _ *v1alpha2.VirtualDisk) func (ds UploadDataSource) Name() string { return uploadDataSource } - -func (ds UploadDataSource) getEnvSettings(vd *v1alpha2.VirtualDisk, supgen supplements.Generator) *uploader.Settings { - var settings uploader.Settings - - uploader.ApplyDVCRDestinationSettings( - &settings, - ds.dvcrSettings, - supgen, - ds.dvcrSettings.RegistryImageForVD(vd), - ) - - return &settings -} - -func (ds UploadDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *cdiv1.DataVolumeSource { - // The image was preloaded from source into dvcr. - // We can't use the same data source a second time, but we can set dvcr as the data source. - // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. - url := common.DockerRegistrySchemePrefix + dvcrSourceImageName - secretName := sup.DVCRAuthSecretForDV().Name - certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } -} - -func (ds UploadDataSource) getPVCSize(vd *v1alpha2.VirtualDisk, pod *corev1.Pod) (resource.Quantity, error) { - // Get size from the importer Pod to detect if specified PVC size is enough. - unpackedSize, err := resource.ParseQuantity(ds.statService.GetSize(pod).UnpackedBytes) - if err != nil { - return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", ds.statService.GetSize(pod).UnpackedBytes, err) - } - - return service.GetValidatedPVCSize(vd.Spec.PersistentVolumeClaim.Size, unpackedSize) -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload_test.go new file mode 100644 index 0000000000..54fa5a25be --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload_test.go @@ -0,0 +1,417 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "errors" + "log/slog" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/common/provisioner" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + vdsupplements "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +var _ = Describe("UploadDataSource", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vd *v1alpha2.VirtualDisk + sc *storagev1.StorageClass + pvc *corev1.PersistentVolumeClaim + disk *UploadDataSourceDiskServiceMock + uploaderSvc *UploadDataSourceUploaderServiceMock + stat *UploadDataSourceStatServiceMock + recorder eventrecord.EventRecorderLogger + dvcrSettings *dvcr.Settings + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(v1alpha2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(storagev1.AddToScheme(scheme)).To(Succeed()) + Expect(netv1.AddToScheme(scheme)).To(Succeed()) + + recorder = &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, _, _ string) {}, + } + + dvcrSettings = &dvcr.Settings{ + RegistryURL: "dvcr.example.com", + } + + sc = &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{Name: "sc"}, + } + + vd = &v1alpha2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vd", + Generation: 1, + UID: "44444444-4444-4444-4444-444444444444", + }, + Spec: v1alpha2.VirtualDiskSpec{ + DataSource: &v1alpha2.VirtualDiskDataSource{ + Type: v1alpha2.DataSourceTypeUpload, + }, + }, + Status: v1alpha2.VirtualDiskStatus{ + StorageClassName: sc.Name, + Target: v1alpha2.DiskTarget{PersistentVolumeClaim: "test-pvc"}, + }, + } + + supgen := vdsupplements.NewGenerator(vd) + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.PersistentVolumeClaim().Name, + Namespace: vd.Namespace, + }, + Spec: corev1.PersistentVolumeClaimSpec{StorageClassName: ptr.To(sc.Name)}, + Status: corev1.PersistentVolumeClaimStatus{ + Capacity: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("1Gi")}, + }, + } + + disk = &UploadDataSourceDiskServiceMock{ + GetCapacityFunc: func(_ *corev1.PersistentVolumeClaim) string { return "1Gi" }, + GetPersistentVolumeClaimFunc: func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return pvc, nil + }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + CleanUpSupplementsFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + } + + uploaderSvc = &UploadDataSourceUploaderServiceMock{ + GetPodFunc: func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return nil, nil }, + GetServiceFunc: func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { return nil, nil }, + GetIngressFunc: func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { return nil, nil }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { return false, nil }, + ProtectFunc: func(_ context.Context, _ supplements.Generator, _ *corev1.Pod, _ *corev1.Service, _ *netv1.Ingress) error { + return nil + }, + GetExternalURLFunc: func(_ context.Context, _ *netv1.Ingress) string { return "https://upload.example.com" }, + GetInClusterURLFunc: func(_ context.Context, _ *corev1.Service) string { return "http://upload.svc/upload" }, + } + + stat = &UploadDataSourceStatServiceMock{ + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { return "dvcr.example.com/cvi/vd:1" }, + GetSizeFunc: func(_ *corev1.Pod) v1alpha2.ImageStatusSize { + return v1alpha2.ImageStatusSize{UnpackedBytes: "500Mi"} + }, + GetFormatFunc: func(_ *corev1.Pod) string { return "qcow2" }, + GetDownloadSpeedFunc: func(_ types.UID, _ *corev1.Pod) *v1alpha2.StatusSpeed { return nil }, + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, prev string, _ ...service.GetProgressOption) string { + if prev == "" { + return "10%" + } + return prev + }, + CheckPodFunc: func(_ *corev1.Pod) error { return nil }, + IsUploadStartedFunc: func(_ types.UID, _ *corev1.Pod) bool { return false }, + IsUploaderReadyFunc: func(_ *corev1.Pod, _ *corev1.Service, _ *netv1.Ingress, _ *corev1.Secret) (bool, error) { + return false, nil + }, + } + }) + + newSyncer := func(c client.Client) *UploadDataSource { + return NewUploadDataSource(recorder, stat, uploaderSvc, disk, dvcrSettings, c) + } + + Context("VirtualDisk has just been created (no uploader supplements yet)", func() { + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + }) + + It("creates the uploader supplements and sets DiskProvisioning", func() { + var started bool + uploaderSvc.StartFunc = func(_ context.Context, _ *uploader.Settings, _ client.Object, _ supplements.Generator, _ *datasource.CABundle, _ ...service.Option) error { + started = true + return nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(started).To(BeTrue()) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(vd.Status.Progress).To(Equal("0%")) + }) + + It("propagates QuotaExceeded as DiskFailed/QuotaExceeded", func() { + uploaderSvc.StartFunc = func(_ context.Context, _ *uploader.Settings, _ client.Object, _ supplements.Generator, _ *datasource.CABundle, _ ...service.Option) error { + return errors.New("exceeded quota: storage requested but limit reached") + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.QuotaExceeded, true) + }) + }) + + Context("Uploader supplements exist, user has not uploaded yet", func() { + var ( + pod *corev1.Pod + svc *corev1.Service + ing *netv1.Ingress + ) + + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "uploader", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } + svc = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "uploader-svc", Namespace: vd.Namespace}} + ing = &netv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "uploader-ing", Namespace: vd.Namespace}} + + uploaderSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + uploaderSvc.GetServiceFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { return svc, nil } + uploaderSvc.GetIngressFunc = func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { return ing, nil } + }) + + It("reports WaitForUserUpload with ImageUploadURLs when the uploader is ready", func() { + stat.IsUploaderReadyFunc = func(_ *corev1.Pod, _ *corev1.Service, _ *netv1.Ingress, _ *corev1.Secret) (bool, error) { + return true, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskWaitForUserUpload)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.WaitForUserUpload, true) + Expect(vd.Status.ImageUploadURLs).ToNot(BeNil()) + Expect(vd.Status.ImageUploadURLs.External).ToNot(BeEmpty()) + Expect(vd.Status.ImageUploadURLs.InCluster).ToNot(BeEmpty()) + }) + + It("reports Provisioning while the uploader is not yet ready", func() { + stat.IsUploaderReadyFunc = func(_ *corev1.Pod, _ *corev1.Service, _ *netv1.Ingress, _ *corev1.Secret) (bool, error) { + return false, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + }) + }) + + Context("User upload is in progress", func() { + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "uploader", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } + uploaderSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + uploaderSvc.GetServiceFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { + return &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "uploader-svc", Namespace: vd.Namespace}}, nil + } + uploaderSvc.GetIngressFunc = func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { + return &netv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "uploader-ing", Namespace: vd.Namespace}}, nil + } + stat.IsUploadStartedFunc = func(_ types.UID, _ *corev1.Pod) bool { return true } + }) + + It("reports DiskProvisioning while uploading to DVCR and protects the uploader", func() { + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + Expect(uploaderSvc.ProtectCalls()).To(HaveLen(1)) + }) + + It("keeps DiskProvisioning on transient uploader pod errors after upload has started", func() { + stat.CheckPodFunc = func(_ *corev1.Pod) error { return service.ErrNotScheduled } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + }) + }) + + Context("Uploader pod has completed, no PVC yet", func() { + BeforeEach(func() { + disk.GetPersistentVolumeClaimFunc = func(_ context.Context, _ supplements.Generator) (*corev1.PersistentVolumeClaim, error) { + return nil, nil + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "uploader", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + uploaderSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + uploaderSvc.GetServiceFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { + return &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "uploader-svc", Namespace: vd.Namespace}}, nil + } + uploaderSvc.GetIngressFunc = func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { + return &netv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "uploader-ing", Namespace: vd.Namespace}}, nil + } + }) + + It("starts the PVC import using a registry source", func() { + var started bool + disk.StartPVCImportFunc = func(_ context.Context, _ resource.Quantity, _ *storagev1.StorageClass, source *service.PVCImportSource, _ *v1alpha2.VirtualDisk, _ *provisioner.NodePlacement) error { + started = true + Expect(source).ToNot(BeNil()) + Expect(source.Registry).ToNot(BeNil()) + return nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(started).To(BeTrue()) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskProvisioning)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Provisioning, true) + }) + + It("fails the disk when the uploaded source is an ISO", func() { + stat.GetFormatFunc = func(_ *corev1.Pod) string { return "iso" } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskFailed)) + ExpectCondition(vd, metav1.ConditionFalse, vdcondition.ProvisioningFailed, true) + }) + }) + + Context("PVC is Bound and the import is complete", func() { + BeforeEach(func() { + pvc.Status.Phase = corev1.ClaimBound + pvc.Annotations = map[string]string{annotations.AnnPVCImportPhase: string(corev1.PodSucceeded)} + }) + + It("marks DiskReady and cleans up the uploader once the condition is finished", func() { + vd.Status.Conditions = []metav1.Condition{{ + Type: vdcondition.ReadyType.String(), + Reason: vdcondition.Ready.String(), + Status: metav1.ConditionTrue, + }} + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "uploader", Namespace: vd.Namespace}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + } + uploaderSvc.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { return pod, nil } + uploaderSvc.GetServiceFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { + return &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "uploader-svc", Namespace: vd.Namespace}}, nil + } + uploaderSvc.GetIngressFunc = func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { + return &netv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "uploader-ing", Namespace: vd.Namespace}}, nil + } + var cleaned bool + uploaderSvc.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + cleaned = true + return true, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + res, err := newSyncer(cl).Sync(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(cleaned).To(BeTrue()) + Expect(vd.Status.Phase).To(Equal(v1alpha2.DiskReady)) + ExpectCondition(vd, metav1.ConditionTrue, vdcondition.Ready, false) + ExpectStats(vd) + }) + }) + + Context("CleanUp", func() { + It("delegates to both uploader and disk services", func() { + var uploaderCleaned, diskCleaned bool + uploaderSvc.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + uploaderCleaned = true + return false, nil + } + disk.CleanUpFunc = func(_ context.Context, _ supplements.Generator) (bool, error) { + diskCleaned = true + return true, nil + } + + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + requeue, err := newSyncer(cl).CleanUp(ctx, vd) + Expect(err).ToNot(HaveOccurred()) + Expect(requeue).To(BeTrue()) + Expect(uploaderCleaned).To(BeTrue()) + Expect(diskCleaned).To(BeTrue()) + }) + }) + + Context("Validate", func() { + It("is a no-op", func() { + cl := fake.NewClientBuilder().WithScheme(scheme).Build() + Expect(newSyncer(cl).Validate(ctx, vd)).To(Succeed()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/supplements/supplements.go b/images/virtualization-artifact/pkg/controller/vd/internal/supplements/supplements.go index 4c43300f69..b9d91aead1 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/supplements/supplements.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/supplements/supplements.go @@ -42,7 +42,7 @@ func (g *VirtualDiskGenerator) SetClaimName(name string) { g.claimName = name } -func (g *VirtualDiskGenerator) DataVolume() types.NamespacedName { +func (g *VirtualDiskGenerator) CommonResourceName() types.NamespacedName { return g.PersistentVolumeClaim() } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/datavolume_watcher.go b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/datavolume_watcher.go deleted file mode 100644 index 84ce2c9082..0000000000 --- a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/datavolume_watcher.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package watcher - -import ( - "fmt" - - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type DataVolumeWatcher struct{} - -func NewDataVolumeWatcher() *DataVolumeWatcher { - return &DataVolumeWatcher{} -} - -func (w *DataVolumeWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &cdiv1.DataVolume{}, - handler.TypedEnqueueRequestForOwner[*cdiv1.DataVolume]( - mgr.GetScheme(), - mgr.GetRESTMapper(), - &v1alpha2.VirtualDisk{}, - handler.OnlyControllerOwner(), - ), - predicate.TypedFuncs[*cdiv1.DataVolume]{ - CreateFunc: func(e event.TypedCreateEvent[*cdiv1.DataVolume]) bool { return false }, - UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.DataVolume]) bool { - if e.ObjectOld.Status.Progress != e.ObjectNew.Status.Progress { - return true - } - - if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase { - switch e.ObjectNew.Status.Phase { - case cdiv1.Succeeded, cdiv1.WaitForFirstConsumer, cdiv1.PendingPopulation: - return true - } - } - - if e.ObjectOld.Status.ClaimName != e.ObjectNew.Status.ClaimName { - return true - } - - oldDVQuotaNotExceeded, oldOk := conditions.GetDataVolumeCondition(conditions.DVQoutaNotExceededConditionType, e.ObjectOld.Status.Conditions) - newDVQuotaNotExceeded, newOk := conditions.GetDataVolumeCondition(conditions.DVQoutaNotExceededConditionType, e.ObjectNew.Status.Conditions) - - if !oldOk && newOk { - return true - } - - if oldOk && newOk && oldDVQuotaNotExceeded != newDVQuotaNotExceeded { - return true - } - - oldDVRunning, _ := conditions.GetDataVolumeCondition(conditions.DVRunningConditionType, e.ObjectOld.Status.Conditions) - newDVRunning, _ := conditions.GetDataVolumeCondition(conditions.DVRunningConditionType, e.ObjectNew.Status.Conditions) - - if oldDVRunning.Reason != newDVRunning.Reason { - return true - } - - return newDVRunning.Reason == "Error" || newDVRunning.Reason == "ImagePullFailed" - }, - }, - ), - ); err != nil { - return fmt.Errorf("error setting watch on DV: %w", err) - } - return nil -} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pod_watcher.go b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pod_watcher.go new file mode 100644 index 0000000000..6e41f5d9b9 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pod_watcher.go @@ -0,0 +1,100 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package watcher + +import ( + "context" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type PodWatcher struct { + logger *log.Logger + client client.Client +} + +func NewPodWatcher(client client.Client) *PodWatcher { + return &PodWatcher{ + logger: log.Default().With("watcher", strings.ToLower("Pod")), + client: client, + } +} + +func (w PodWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch( + source.Kind(mgr.GetCache(), &corev1.Pod{}, + handler.TypedEnqueueRequestsFromMapFunc(w.enqueueRequestsFromPVC), + predicate.TypedFuncs[*corev1.Pod]{ + UpdateFunc: func(e event.TypedUpdateEvent[*corev1.Pod]) bool { + return e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase + }, + CreateFunc: func(e event.TypedCreateEvent[*corev1.Pod]) bool { return false }, + DeleteFunc: func(e event.TypedDeleteEvent[*corev1.Pod]) bool { return false }, + }, + ), + ); err != nil { + return fmt.Errorf("error setting watch on Pod: %w", err) + } + return nil +} + +func (w PodWatcher) enqueueRequestsFromPVC(ctx context.Context, pod *corev1.Pod) []reconcile.Request { + for _, ownerRef := range pod.OwnerReferences { + if ownerRef.Kind == v1alpha2.VirtualDiskKind { + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{Name: ownerRef.Name, Namespace: pod.Namespace}, + }} + } + + if ownerRef.Kind != "PersistentVolumeClaim" { + continue + } + + target, err := object.FetchObject(ctx, types.NamespacedName{Name: ownerRef.Name, Namespace: pod.Namespace}, w.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + w.logger.Error(fmt.Sprintf("failed to fetch pod owner pvc: %s", err)) + continue + } + if target == nil { + continue + } + + for _, pvcOwnerRef := range target.OwnerReferences { + if pvcOwnerRef.Kind == v1alpha2.VirtualDiskKind { + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{Name: pvcOwnerRef.Name, Namespace: target.Namespace}, + }} + } + } + } + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pvc_watcher.go b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pvc_watcher.go index c771577ce1..96e185bc1f 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pvc_watcher.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/pvc_watcher.go @@ -24,7 +24,6 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" @@ -35,8 +34,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/deckhouse/deckhouse/pkg/log" - "github.com/deckhouse/virtualization-controller/pkg/common/datavolume" - "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -69,31 +66,15 @@ func (w PersistentVolumeClaimWatcher) Watch(mgr manager.Manager, ctr controller. return nil } -func (w PersistentVolumeClaimWatcher) enqueueRequestsFromOwnerRefsRecursively(ctx context.Context, obj client.Object) (requests []reconcile.Request) { +func (w PersistentVolumeClaimWatcher) enqueueRequestsFromOwnerRefsRecursively(_ context.Context, obj client.Object) (requests []reconcile.Request) { for _, ownerRef := range obj.GetOwnerReferences() { - switch ownerRef.Kind { - case v1alpha2.VirtualDiskKind: + if ownerRef.Kind == v1alpha2.VirtualDiskKind { requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: ownerRef.Name, Namespace: obj.GetNamespace(), }, }) - case datavolume.DataVolumeKind: - dv, err := object.FetchObject(ctx, types.NamespacedName{ - Name: ownerRef.Name, - Namespace: obj.GetNamespace(), - }, w.client, &cdiv1.DataVolume{}) - if err != nil { - w.logger.Error(fmt.Sprintf("failed to fetch dv: %s", err)) - continue - } - - if dv == nil { - continue - } - - requests = append(requests, w.enqueueRequestsFromOwnerRefsRecursively(ctx, dv)...) } } diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go index 5eae311fdf..d16ecae5d1 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go @@ -58,6 +58,7 @@ func NewController( mgr manager.Manager, log *log.Logger, importerImage string, + diskImporterImage string, uploaderImage string, requirements corev1.ResourceRequirements, dvcr *dvcr.Settings, @@ -67,7 +68,12 @@ func NewController( protection := service.NewProtectionService(mgr.GetClient(), v1alpha2.FinalizerVDProtection) importer := service.NewImporterService(dvcr, mgr.GetClient(), importerImage, requirements, PodPullPolicy, PodVerbose, ControllerName, protection) uploader := service.NewUploaderService(dvcr, mgr.GetClient(), uploaderImage, requirements, PodPullPolicy, PodVerbose, ControllerName, protection) - disk := service.NewDiskService(mgr.GetClient(), dvcr, protection, ControllerName) + disk := service.NewDiskService(mgr.GetClient(), dvcr, protection, ControllerName, service.DiskImporterConfig{ + Image: diskImporterImage, + ResourceRequirements: requirements, + PullPolicy: PodPullPolicy, + Verbose: PodVerbose, + }) scService := intsvc.NewVirtualDiskStorageClassService(service.NewBaseStorageClassService(mgr.GetClient()), storageClassSettings) dvcrService := service.NewDVCRService(mgr.GetClient()) recorder := eventrecord.NewEventRecorderLogger(mgr, ControllerName) @@ -77,7 +83,7 @@ func NewController( sources := source.NewSources() sources.Set(v1alpha2.DataSourceTypeHTTP, source.NewHTTPDataSource(recorder, stat, importer, disk, dvcr, mgr.GetClient())) sources.Set(v1alpha2.DataSourceTypeContainerImage, source.NewRegistryDataSource(recorder, stat, importer, disk, dvcr, mgr.GetClient())) - sources.Set(v1alpha2.DataSourceTypeObjectRef, source.NewObjectRefDataSource(recorder, disk, mgr.GetClient())) + sources.Set(v1alpha2.DataSourceTypeObjectRef, source.NewObjectRefDataSource(recorder, stat, disk, mgr.GetClient())) sources.Set(v1alpha2.DataSourceTypeUpload, source.NewUploadDataSource(recorder, stat, uploader, disk, dvcr, mgr.GetClient())) reconciler := NewReconciler( diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go b/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go index 3a36204b6c..e218bca711 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go @@ -103,9 +103,9 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr for _, w := range []Watcher{ watcher.NewVirtualDiskWatcher(), watcher.NewPersistentVolumeClaimWatcher(mgrClient), + watcher.NewPodWatcher(mgrClient), watcher.NewVirtualDiskSnapshotWatcher(mgrClient), watcher.NewStorageClassWatcher(mgrClient), - watcher.NewDataVolumeWatcher(), watcher.NewVirtualMachineWatcher(), watcher.NewResourceQuotaWatcher(mgrClient), postponeimporter.NewWatcher[*v1alpha2.VirtualDisk](mgrClient, logger), diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service.go b/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service.go index 854e1eedc1..9c594f5b48 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service.go @@ -127,13 +127,15 @@ func (svc *VirtualImageStorageClassService) ValidateClaimPropertySets(sp *cdiv1. } for _, cps := range sp.Status.ClaimPropertySets { - if slices.Contains(cps.AccessModes, corev1.ReadWriteMany) && *cps.VolumeMode == corev1.PersistentVolumeBlock { + if (slices.Contains(cps.AccessModes, corev1.ReadWriteOnce) || slices.Contains(cps.AccessModes, corev1.ReadWriteMany)) && + cps.VolumeMode != nil && + *cps.VolumeMode == corev1.PersistentVolumeBlock { return nil } } return fmt.Errorf( - "the storage class %q lacks of capabilities to support 'Virtual Images on PVC' function; use StorageClass that supports volume mode 'Block' and access mode 'ReadWriteMany'", + "the storage class %q lacks of capabilities to support 'Virtual Images on PVC' function; use StorageClass that supports volume mode 'Block' and access mode 'ReadWriteOnce' or 'ReadWriteMany'", sp.Name, ) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service_test.go index a4146734f7..79bb47c0f1 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/service/vi_storage_class_service_test.go @@ -254,7 +254,7 @@ var _ = Describe("VirtualImageStorageClassService", func() { }) }) When("a storage profile has the volume mode `Block` and the access mode `ReadWriteOnce`", func() { - It("returns an error", func() { + It("does not return an error", func() { sp := &cdiv1.StorageProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "BlockStorageClass", @@ -274,7 +274,7 @@ var _ = Describe("VirtualImageStorageClassService", func() { }, } err := service.ValidateClaimPropertySets(sp) - Expect(err).To(HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) }) }) }) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go index fb5e3915a5..09ebbe75db 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go @@ -23,10 +23,8 @@ import ( "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common" @@ -200,48 +198,30 @@ func (ds HTTPDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualIma if err != nil { return reconcile.Result{}, err } - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { return reconcile.Result{}, err } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - } - switch { case IsImageProvisioningFinished(condition): log.Info("Image provisioning finished: clean up") setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - // Unprotect import time supplements to delete them later. err = ds.importerService.Unprotect(ctx, pod, supgen) if err != nil { return reconcile.Result{}, err } - err = ds.diskService.Unprotect(ctx, supgen, dv) + err = ds.diskService.Unprotect(ctx, supgen) if err != nil { return reconcile.Result{}, err } return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(pod, dv, pvc): + case object.AnyTerminating(pod, pvc): log.Info("Waiting for supplements to be terminated") case pod == nil: vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) @@ -289,122 +269,15 @@ func (ds HTTPDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualIma vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress, service.NewScaleOption(0, 50)) vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) - case dv == nil: + default: ds.recorder.Event( vi, corev1.EventTypeNormal, v1alpha2.ReasonDataSourceSyncStarted, "The HTTP DataSource import has started", ) - - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = v1alpha2.ImageFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - vi.Status.Progress = "50.0%" - vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) - - var diskSize resource.Quantity - diskSize, err = ds.getPVCSize(pod) - if err != nil { - setPhaseConditionToFailed(cb, &vi.Status.Phase, err) - - if errors.Is(err, service.ErrInsufficientPVCSize) { - return reconcile.Result{}, nil - } - - return reconcile.Result{}, err - } - source := ds.getSource(supgen, ds.statService.GetDVCRImageName(pod)) - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, diskSize, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The HTTP DataSource import has completed", - ) - - vi.Status.Phase = v1alpha2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Progress = "100%" - vi.Status.Size = ds.statService.GetSize(pod) - vi.Status.CDROM = ds.statService.GetCDROM(pod) - vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(50, 100)) - - err = ds.diskService.Protect(ctx, supgen, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + return reconcilePVCImportFromDVCR(ctx, vi, pod, pvc, source, cb, supgen, ds.statService, ds.diskService) } return reconcile.Result{RequeueAfter: time.Second}, nil @@ -478,7 +351,7 @@ func (ds HTTPDataSource) getPVCSize(pod *corev1.Pod) (resource.Quantity, error) return service.GetValidatedPVCSize(&unpackedSize, unpackedSize) } -func (ds HTTPDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *cdiv1.DataVolumeSource { +func (ds HTTPDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *service.PVCImportSource { // The image was preloaded from source into dvcr. // We can't use the same data source a second time, but we can set dvcr as the data source. // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. @@ -486,11 +359,5 @@ func (ds HTTPDataSource) getSource(sup supplements.Generator, dvcrSourceImageNam secretName := sup.DVCRAuthSecretForDV().Name certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go index ed1b52f60f..536d6e1aae 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go @@ -23,12 +23,10 @@ import ( "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -131,20 +129,15 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.Virtu } supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { return reconcile.Result{}, err } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) + var dvcrDataSource controller.DVCRDataSource + dvcrDataSource, err = controller.NewDVCRDataSourcesForVMI(ctx, vi.Spec.DataSource, vi, ds.client) + if err != nil { + return reconcile.Result{}, err } switch { @@ -153,21 +146,15 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.Virtu setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, supgen, dv) + err = ds.diskService.Unprotect(ctx, supgen) if err != nil { return reconcile.Result{}, err } return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(dv, pvc): + case object.AnyTerminating(pvc): log.Info("Waiting for supplements to be terminated") - case dv == nil: + default: ds.recorder.Event( vi, corev1.EventTypeNormal, @@ -175,12 +162,6 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.Virtu "The ObjectRef DataSource import has started", ) - var dvcrDataSource controller.DVCRDataSource - dvcrDataSource, err = controller.NewDVCRDataSourcesForVMI(ctx, vi.Spec.DataSource, vi, ds.client) - if err != nil { - return reconcile.Result{}, err - } - if !dvcrDataSource.IsReady() { cb. Status(metav1.ConditionFalse). @@ -204,94 +185,18 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.Virtu return reconcile.Result{}, err } - var source *cdiv1.DataVolumeSource + var source *service.PVCImportSource source, err = ds.getSource(supgen, dvcrDataSource) if err != nil { return reconcile.Result{}, err } - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, diskSize, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The ObjectRef DataSource import has completed", - ) - - vi.Status.Phase = v1alpha2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - var dvcrDataSource controller.DVCRDataSource - dvcrDataSource, err = controller.NewDVCRDataSourcesForVMI(ctx, vi.Spec.DataSource, vi, ds.client) - if err != nil { - return reconcile.Result{}, err - } - - vi.Status.Size = dvcrDataSource.GetSize() - vi.Status.CDROM = dvcrDataSource.IsCDROM() - vi.Status.Format = dvcrDataSource.GetFormat() - vi.Status.Progress = "100%" - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(0, 100)) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - err = ds.diskService.Protect(ctx, supgen, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + return reconcilePVCImportFromReadySource(ctx, vi, pvc, source, diskSize, cb, supgen, ds.diskService, func() { + ds.recorder.Event(vi, corev1.EventTypeNormal, v1alpha2.ReasonDataSourceSyncCompleted, "The ObjectRef DataSource import has completed") + vi.Status.Size = dvcrDataSource.GetSize() + vi.Status.CDROM = dvcrDataSource.IsCDROM() + vi.Status.Format = dvcrDataSource.GetFormat() + }) } return reconcile.Result{RequeueAfter: time.Second}, nil @@ -602,7 +507,7 @@ func (ds ObjectRefDataSource) getPVCSize(dvcrDataSource controller.DVCRDataSourc return service.GetValidatedPVCSize(&unpackedSize, unpackedSize) } -func (ds ObjectRefDataSource) getSource(sup supplements.Generator, dvcrDataSource controller.DVCRDataSource) (*cdiv1.DataVolumeSource, error) { +func (ds ObjectRefDataSource) getSource(sup supplements.Generator, dvcrDataSource controller.DVCRDataSource) (*service.PVCImportSource, error) { if !dvcrDataSource.IsReady() { return nil, errors.New("dvcr data source is not ready") } @@ -611,11 +516,5 @@ func (ds ObjectRefDataSource) getSource(sup supplements.Generator, dvcrDataSourc secretName := sup.DVCRAuthSecretForDV().Name certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - }, nil + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName), nil } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go index 9a0840fe42..2ba831fa17 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vd.go @@ -24,12 +24,10 @@ import ( "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -222,23 +220,11 @@ func (ds ObjectRefVirtualDisk) StoreToPVC(ctx context.Context, vi *v1alpha2.Virt log, ctx := logger.GetDataSourceContext(ctx, objectRefDataSource) supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { return reconcile.Result{}, err } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - } - condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) switch { case IsImageProvisioningFinished(condition): @@ -246,21 +232,15 @@ func (ds ObjectRefVirtualDisk) StoreToPVC(ctx context.Context, vi *v1alpha2.Virt setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, supgen, dv) + err = ds.diskService.Unprotect(ctx, supgen) if err != nil { return reconcile.Result{}, err } return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(dv, pvc): + case object.AnyTerminating(pvc): log.Info("Waiting for supplements to be terminated") - case dv == nil: + default: ds.recorder.Event( vi, corev1.EventTypeNormal, @@ -271,12 +251,7 @@ func (ds ObjectRefVirtualDisk) StoreToPVC(ctx context.Context, vi *v1alpha2.Virt vi.Status.Progress = "0%" vi.Status.SourceUID = ptr.To(vdRef.GetUID()) - source := &cdiv1.DataVolumeSource{ - PVC: &cdiv1.DataVolumeSourcePVC{ - Name: vdRef.Status.Target.PersistentVolumeClaim, - Namespace: vdRef.Namespace, - }, - } + source := service.NewPVCPVCImportSource(vdRef.Status.Target.PersistentVolumeClaim, vdRef.Namespace) var size resource.Quantity size, err = resource.ParseQuantity(vdRef.Status.Capacity) @@ -284,97 +259,24 @@ func (ds ObjectRefVirtualDisk) StoreToPVC(ctx context.Context, vi *v1alpha2.Virt return reconcile.Result{}, err } - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, size, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The ObjectRef DataSource import has completed", - ) - - vi.Status.Phase = v1alpha2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - q, err := resource.ParseQuantity(vdRef.Status.Capacity) - if err != nil { - return reconcile.Result{}, err - } - - intQ, ok := q.AsInt64() - if !ok { - return reconcile.Result{}, errors.New("fail to convert quantity to int64") - } - - vi.Status.Size = v1alpha2.ImageStatusSize{ - Stored: vdRef.Status.Capacity, - StoredBytes: strconv.FormatInt(intQ, 10), - Unpacked: vdRef.Status.Capacity, - UnpackedBytes: strconv.FormatInt(intQ, 10), - } - - vi.Status.Format = imageformat.FormatRAW - vi.Status.Progress = "100%" - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(0, 100)) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - err = ds.diskService.Protect(ctx, supgen, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + return reconcilePVCImportFromReadySource(ctx, vi, pvc, source, size, cb, supgen, ds.diskService, func() { + ds.recorder.Event(vi, corev1.EventTypeNormal, v1alpha2.ReasonDataSourceSyncCompleted, "The ObjectRef DataSource import has completed") + q, err := resource.ParseQuantity(vdRef.Status.Capacity) + if err != nil { + return + } + intQ, ok := q.AsInt64() + if !ok { + return + } + vi.Status.Size = v1alpha2.ImageStatusSize{ + Stored: vdRef.Status.Capacity, + StoredBytes: strconv.FormatInt(intQ, 10), + Unpacked: vdRef.Status.Capacity, + UnpackedBytes: strconv.FormatInt(intQ, 10), + } + vi.Status.Format = imageformat.FormatRAW + }) } return reconcile.Result{RequeueAfter: time.Second}, nil diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go index ad56a5175c..205860091a 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vi_on_pvc.go @@ -23,11 +23,9 @@ import ( "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -195,22 +193,11 @@ func (ds ObjectRefDataVirtualImageOnPVC) StoreToPVC(ctx context.Context, vi, viR log, _ := logger.GetDataSourceContext(ctx, objectRefDataSource) supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { return reconcile.Result{}, err } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - } - condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) switch { case IsImageProvisioningFinished(condition): @@ -218,21 +205,15 @@ func (ds ObjectRefDataVirtualImageOnPVC) StoreToPVC(ctx context.Context, vi, viR setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.diskService.Unprotect(ctx, supgen, dv) + err = ds.diskService.Unprotect(ctx, supgen) if err != nil { return reconcile.Result{}, err } return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(dv, pvc): + case object.AnyTerminating(pvc): log.Info("Waiting for supplements to be terminated") - case dv == nil: + default: ds.recorder.Event( vi, corev1.EventTypeNormal, @@ -255,88 +236,14 @@ func (ds ObjectRefDataVirtualImageOnPVC) StoreToPVC(ctx context.Context, vi, viR return reconcile.Result{}, err } - source := &cdiv1.DataVolumeSource{ - PVC: &cdiv1.DataVolumeSourcePVC{ - Name: viRef.Status.Target.PersistentVolumeClaim, - Namespace: viRef.Namespace, - }, - } - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, size, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The ObjectRef DataSource import has completed", - ) - - vi.Status.Phase = v1alpha2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - vi.Status.Size = viRef.Status.Size - vi.Status.CDROM = viRef.Status.CDROM - vi.Status.Format = viRef.Status.Format - vi.Status.Progress = "100%" - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(0, 100)) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - err = ds.diskService.Protect(ctx, supgen, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } + source := service.NewPVCPVCImportSource(viRef.Status.Target.PersistentVolumeClaim, viRef.Namespace) - return reconcile.Result{}, nil + return reconcilePVCImportFromReadySource(ctx, vi, pvc, source, size, cb, supgen, ds.diskService, func() { + ds.recorder.Event(vi, corev1.EventTypeNormal, v1alpha2.ReasonDataSourceSyncCompleted, "The ObjectRef DataSource import has completed") + vi.Status.Size = viRef.Status.Size + vi.Status.CDROM = viRef.Status.CDROM + vi.Status.Format = viRef.Status.Format + }) } return reconcile.Result{RequeueAfter: time.Second}, nil diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go index 6669fc7f6b..c1b0360b44 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go @@ -23,11 +23,9 @@ import ( "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -88,47 +86,30 @@ func (ds RegistryDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.Virtua if err != nil { return reconcile.Result{}, err } - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { return reconcile.Result{}, err } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - } - switch { case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - // Unprotect import time supplements to delete them later. err = ds.importerService.Unprotect(ctx, pod, supgen) if err != nil { return reconcile.Result{}, err } - err = ds.diskService.Unprotect(ctx, supgen, dv) + err = ds.diskService.Unprotect(ctx, supgen) if err != nil { return reconcile.Result{}, err } return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(pod, dv, pvc): + case object.AnyTerminating(pod, pvc): log.Info("Waiting for supplements to be terminated") case pod == nil: log.Info("Start import to DVCR") @@ -175,112 +156,10 @@ func (ds RegistryDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.Virtua if err != nil { return reconcile.Result{}, err } - case dv == nil: + default: log.Info("Start import to PVC") - - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = v1alpha2.ImageFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - vi.Status.Progress = "50.0%" - - var diskSize resource.Quantity - diskSize, err = ds.getPVCSize(pod) - if err != nil { - setPhaseConditionToFailed(cb, &vi.Status.Phase, err) - - if errors.Is(err, service.ErrInsufficientPVCSize) { - return reconcile.Result{}, nil - } - - return reconcile.Result{}, err - } - source := ds.getSource(supgen, ds.statService.GetDVCRImageName(pod)) - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, diskSize, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Phase = v1alpha2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Progress = "100%" - vi.Status.Size = ds.statService.GetSize(pod) - vi.Status.CDROM = ds.statService.GetCDROM(pod) - vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(50, 100)) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - err = ds.diskService.Protect(ctx, supgen, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + return reconcilePVCImportFromDVCR(ctx, vi, pod, pvc, source, cb, supgen, ds.statService, ds.diskService) } return reconcile.Result{RequeueAfter: time.Second}, nil @@ -495,7 +374,7 @@ func (ds RegistryDataSource) getPVCSize(pod *corev1.Pod) (resource.Quantity, err return service.GetValidatedPVCSize(&unpackedSize, unpackedSize) } -func (ds RegistryDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *cdiv1.DataVolumeSource { +func (ds RegistryDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *service.PVCImportSource { // The image was preloaded from source into dvcr. // We can't use the same data source a second time, but we can set dvcr as the data source. // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. @@ -503,11 +382,5 @@ func (ds RegistryDataSource) getSource(sup supplements.Generator, dvcrSourceImag secretName := sup.DVCRAuthSecretForDV().Name certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/source_additional_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/source_additional_test.go index 48ef06ecc4..b184a4be45 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/source_additional_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/source_additional_test.go @@ -389,7 +389,7 @@ var _ = Describe("Source validations and helpers", func() { source, err := ds.getSource(supgen, dvcrSource) Expect(err).ToNot(HaveOccurred()) - Expect(*source.Registry.URL).To(Equal("docker://registry.example/source")) + Expect(source.Registry.URL).To(Equal("docker://registry.example/source")) }) It("rejects not ready dvcr source in helper methods", func() { @@ -441,7 +441,7 @@ var _ = Describe("Source validations and helpers", func() { q, err := registry.getPVCSize(&corev1.Pod{}) Expect(err).ToNot(HaveOccurred()) Expect(q).To(Equal(resource.MustParse("2Gi"))) - Expect(*registry.getSource(supgen, "registry.example/image").Registry.URL).To(Equal("docker://registry.example/image")) + Expect(registry.getSource(supgen, "registry.example/image").Registry.URL).To(Equal("docker://registry.example/image")) }) It("covers http helpers", func() { @@ -455,7 +455,7 @@ var _ = Describe("Source validations and helpers", func() { q, err := httpDS.getPVCSize(&corev1.Pod{}) Expect(err).ToNot(HaveOccurred()) Expect(q).To(Equal(resource.MustParse("3Gi"))) - Expect(*httpDS.getSource(supgen, "registry.example/image").Registry.URL).To(Equal("docker://registry.example/image")) + Expect(httpDS.getSource(supgen, "registry.example/image").Registry.URL).To(Equal("docker://registry.example/image")) }) It("covers upload helpers", func() { @@ -469,7 +469,7 @@ var _ = Describe("Source validations and helpers", func() { q, err := upload.getPVCSize(&corev1.Pod{}) Expect(err).ToNot(HaveOccurred()) Expect(q).To(Equal(resource.MustParse("4Gi"))) - Expect(*upload.getSource(supgen, "registry.example/image").Registry.URL).To(Equal("docker://registry.example/image")) + Expect(upload.getSource(supgen, "registry.example/image").Registry.URL).To(Equal("docker://registry.example/image")) }) }) @@ -481,5 +481,17 @@ var _ = Describe("Source validations and helpers", func() { Expect(phase).To(Equal(v1alpha2.ImageFailed)) Expect(cb.Condition().Message).To(Equal("Plain error")) }) + + It("keeps upload status in Provisioning after upload progress appears", func() { + cb := conditions.NewConditionBuilder(vicondition.ReadyType) + vi := newVI() + vi.Status.Progress = "41.9%" + + Expect(hasUploadProgress(vi.Status.Progress)).To(BeTrue()) + setUploadProvisioningPhaseCondition(cb, vi) + + Expect(vi.Status.Phase).To(Equal(v1alpha2.ImageProvisioning)) + Expect(cb.Condition().Reason).To(Equal(vicondition.Provisioning.String())) + }) }) }) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go index 5292845849..219cc64ac7 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go @@ -23,8 +23,8 @@ import ( "time" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/object" @@ -106,10 +106,6 @@ func IsImageProvisioningFinished(c metav1.Condition) bool { return c.Reason == vicondition.Ready.String() } -type CheckImportProcess interface { - CheckImportProcess(ctx context.Context, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim) error -} - func setPhaseConditionForFinishedImage( pvc *corev1.PersistentVolumeClaim, cb *conditions.ConditionBuilder, @@ -140,51 +136,6 @@ func setPhaseConditionToFailed(cb *conditions.ConditionBuilder, phase *v1alpha2. Message(service.CapitalizeFirstLetter(err.Error())) } -func setPhaseConditionForPVCProvisioningImage( - ctx context.Context, - dv *cdiv1.DataVolume, - vi *v1alpha2.VirtualImage, - pvc *corev1.PersistentVolumeClaim, - cb *conditions.ConditionBuilder, - checker CheckImportProcess, -) error { - err := checker.CheckImportProcess(ctx, dv, pvc) - switch { - case err == nil: - if dv == nil { - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Waiting for the pvc importer to be created") - return nil - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Import is in the process of provisioning to PVC.") - return nil - case errors.Is(err, service.ErrDataVolumeNotRunning): - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error())) - return nil - case errors.Is(err, service.ErrDefaultStorageClassNotFound): - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message("Default StorageClass not found in the cluster: please provide a StorageClass name or set a default StorageClass.") - return nil - default: - return err - } -} - func setPhaseConditionFromPodError(cb *conditions.ConditionBuilder, vi *v1alpha2.VirtualImage, err error) error { vi.Status.Phase = v1alpha2.ImageFailed @@ -229,6 +180,148 @@ func setPhaseConditionFromStorageError(err error, vi *v1alpha2.VirtualImage, cb } } +func reconcilePVCImportFromDVCR( + ctx context.Context, + vi *v1alpha2.VirtualImage, + pod *corev1.Pod, + pvc *corev1.PersistentVolumeClaim, + source *service.PVCImportSource, + cb *conditions.ConditionBuilder, + supgen supplements.Generator, + stat Stat, + disk *service.DiskService, +) (reconcile.Result, error) { + if pvc == nil { + if err := stat.CheckPod(pod); err != nil { + vi.Status.Phase = v1alpha2.ImageFailed + switch { + case errors.Is(err, service.ErrProvisioningFailed): + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(err.Error() + ".")) + return reconcile.Result{}, nil + default: + return reconcile.Result{}, err + } + } + + vi.Status.Progress = "50.0%" + vi.Status.DownloadSpeed = stat.GetDownloadSpeed(vi.GetUID(), pod) + + diskSize, err := getPVCSizeFromPod(stat, pod) + if err != nil { + setPhaseConditionToFailed(cb, &vi.Status.Phase, err) + if errors.Is(err, service.ErrInsufficientPVCSize) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, err + } + + sc, err := disk.GetStorageClass(ctx, vi.Status.StorageClassName) + if err != nil { + return reconcile.Result{}, err + } + err = disk.StartSupplementPVCImport(ctx, diskSize, sc, source, vi, supgen, nil) + if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { + return reconcile.Result{}, err + } + + vi.Status.Phase = v1alpha2.ImageProvisioning + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("PVC Provisioner not found: create the new one.") + return reconcile.Result{RequeueAfter: time.Second}, nil + } + + phase, err := disk.EnsureSupplementPVCImport(ctx, pvc, source, vi, supgen, nil) + if err != nil { + return reconcile.Result{}, err + } + + vi.Status.Target.PersistentVolumeClaim = pvc.Name + switch phase { + case corev1.PodSucceeded: + vi.Status.Phase = v1alpha2.ImageReady + cb.Status(metav1.ConditionTrue).Reason(vicondition.Ready).Message("") + vi.Status.Progress = "100%" + vi.Status.Size = stat.GetSize(pod) + vi.Status.CDROM = stat.GetCDROM(pod) + vi.Status.DownloadSpeed = stat.GetDownloadSpeed(vi.GetUID(), pod) + return reconcile.Result{RequeueAfter: time.Second}, nil + case corev1.PodFailed: + vi.Status.Phase = v1alpha2.ImageFailed + cb.Status(metav1.ConditionFalse).Reason(vicondition.ProvisioningFailed).Message("VirtualImage importer Pod failed.") + return reconcile.Result{}, nil + default: + vi.Status.Phase = v1alpha2.ImageProvisioning + cb.Status(metav1.ConditionFalse).Reason(vicondition.Provisioning).Message("Import is in the process of provisioning to PVC.") + vi.Status.Progress = "50.0%" + return reconcile.Result{RequeueAfter: time.Second}, nil + } +} + +func getPVCSizeFromPod(stat Stat, pod *corev1.Pod) (resource.Quantity, error) { + unpackedSize, err := resource.ParseQuantity(stat.GetSize(pod).UnpackedBytes) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to parse unpacked bytes %s: %w", stat.GetSize(pod).UnpackedBytes, err) + } + if unpackedSize.IsZero() { + return resource.Quantity{}, errors.New("got zero unpacked size from data source") + } + return service.GetValidatedPVCSize(&unpackedSize, unpackedSize) +} + +func reconcilePVCImportFromReadySource( + ctx context.Context, + vi *v1alpha2.VirtualImage, + pvc *corev1.PersistentVolumeClaim, + source *service.PVCImportSource, + size resource.Quantity, + cb *conditions.ConditionBuilder, + supgen supplements.Generator, + disk *service.DiskService, + ready func(), +) (reconcile.Result, error) { + if pvc == nil { + sc, err := disk.GetStorageClass(ctx, vi.Status.StorageClassName) + if err != nil { + return reconcile.Result{}, err + } + err = disk.StartSupplementPVCImport(ctx, size, sc, source, vi, supgen, nil) + if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { + return reconcile.Result{}, err + } + vi.Status.Phase = v1alpha2.ImageProvisioning + cb.Status(metav1.ConditionFalse).Reason(vicondition.Provisioning).Message("PVC Provisioner not found: create the new one.") + return reconcile.Result{RequeueAfter: time.Second}, nil + } + + phase, err := disk.EnsureSupplementPVCImport(ctx, pvc, source, vi, supgen, nil) + if err != nil { + return reconcile.Result{}, err + } + vi.Status.Target.PersistentVolumeClaim = pvc.Name + switch phase { + case corev1.PodSucceeded: + vi.Status.Phase = v1alpha2.ImageReady + cb.Status(metav1.ConditionTrue).Reason(vicondition.Ready).Message("") + vi.Status.Progress = "100%" + ready() + return reconcile.Result{RequeueAfter: time.Second}, nil + case corev1.PodFailed: + vi.Status.Phase = v1alpha2.ImageFailed + cb.Status(metav1.ConditionFalse).Reason(vicondition.ProvisioningFailed).Message("VirtualImage importer Pod failed.") + return reconcile.Result{}, nil + default: + vi.Status.Phase = v1alpha2.ImageProvisioning + vi.Status.Progress = "0%" + cb.Status(metav1.ConditionFalse).Reason(vicondition.Provisioning).Message("Import is in the process of provisioning to PVC.") + return reconcile.Result{RequeueAfter: time.Second}, nil + } +} + const retryPeriod = 1 func setQuotaExceededPhaseCondition(cb *conditions.ConditionBuilder, phase *v1alpha2.ImagePhase, err error, creationTimestamp metav1.Time) reconcile.Result { @@ -245,10 +338,3 @@ func setQuotaExceededPhaseCondition(cb *conditions.ConditionBuilder, phase *v1al cb.Message(fmt.Sprintf("Quota exceeded: %s; Retry in %d minute.", err, retryPeriod)) return reconcile.Result{RequeueAfter: retryPeriod * time.Minute} } - -const ( - DVRunningConditionType cdiv1.DataVolumeConditionType = "Running" - DVQoutaNotExceededConditionType cdiv1.DataVolumeConditionType = "QuotaNotExceeded" - - DVImagePullFailedReason = "ImagePullFailed" -) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go index 76686dc8dd..c51ea57dfd 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources_test.go @@ -25,7 +25,6 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/annotations" @@ -79,14 +78,6 @@ func (s *sourcesCleanerStub) CleanUpSupplements(context.Context, *v1alpha2.Virtu return s.cleanupSupplementsResult, s.cleanupSupplementsErr } -type sourcesImportCheckerStub struct { - err error -} - -func (s sourcesImportCheckerStub) CheckImportProcess(context.Context, *cdiv1.DataVolume, *corev1.PersistentVolumeClaim) error { - return s.err -} - var _ = Describe("Sources helpers", func() { newVI := func() *v1alpha2.VirtualImage { return &v1alpha2.VirtualImage{ @@ -216,39 +207,6 @@ var _ = Describe("Sources helpers", func() { Entry("marks image ready when pvc exists", &corev1.PersistentVolumeClaim{}, v1alpha2.ImageReady, metav1.ConditionTrue, vicondition.Ready.String(), ""), ) - DescribeTable( - "setPhaseConditionForPVCProvisioningImage", - func( - dv *cdiv1.DataVolume, - checkerErr error, - expectedPhase v1alpha2.ImagePhase, - expectedStatus metav1.ConditionStatus, - expectedReason string, - expectedMessage string, - expectedErr error, - ) { - vi := newVI() - cb := conditions.NewConditionBuilder(vicondition.ReadyType) - - err := setPhaseConditionForPVCProvisioningImage(context.Background(), dv, vi, nil, cb, sourcesImportCheckerStub{err: checkerErr}) - if expectedErr == nil { - Expect(err).ToNot(HaveOccurred()) - } else { - Expect(err).To(MatchError(expectedErr)) - } - - Expect(vi.Status.Phase).To(Equal(expectedPhase)) - Expect(cb.Condition().Status).To(Equal(expectedStatus)) - Expect(cb.Condition().Reason).To(Equal(expectedReason)) - Expect(cb.Condition().Message).To(Equal(expectedMessage)) - }, - Entry("waits for pvc importer creation when dv is absent", nil, nil, v1alpha2.ImageProvisioning, metav1.ConditionFalse, vicondition.Provisioning.String(), "Waiting for the pvc importer to be created", nil), - Entry("reports provisioning in progress", &cdiv1.DataVolume{}, nil, v1alpha2.ImageProvisioning, metav1.ConditionFalse, vicondition.Provisioning.String(), "Import is in the process of provisioning to PVC.", nil), - Entry("handles data volume not running", &cdiv1.DataVolume{}, service.ErrDataVolumeNotRunning, v1alpha2.ImageProvisioning, metav1.ConditionFalse, vicondition.ProvisioningFailed.String(), "Pvc importer is not running", nil), - Entry("handles missing default storage class", &cdiv1.DataVolume{}, service.ErrDefaultStorageClassNotFound, v1alpha2.ImagePending, metav1.ConditionFalse, vicondition.ProvisioningFailed.String(), "Default StorageClass not found in the cluster: please provide a StorageClass name or set a default StorageClass.", nil), - Entry("returns unexpected error", &cdiv1.DataVolume{}, errors.New("boom"), v1alpha2.ImagePhase(""), metav1.ConditionUnknown, conditions.ReasonUnknown.String(), "", errors.New("boom")), - ) - DescribeTable( "setPhaseConditionFromPodError", func( diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go index 2404cccea4..65f651e515 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go @@ -23,10 +23,8 @@ import ( "time" corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -95,10 +93,6 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI if err != nil { return reconcile.Result{}, err } - dv, err := ds.diskService.GetDataVolume(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) if err != nil { return reconcile.Result{}, err @@ -114,38 +108,25 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI return reconcile.Result{}, err } - var dvQuotaNotExceededCondition *cdiv1.DataVolumeCondition - var dvRunningCondition *cdiv1.DataVolumeCondition - if dv != nil { - dvQuotaNotExceededCondition = service.GetDataVolumeCondition(DVQoutaNotExceededConditionType, dv.Status.Conditions) - dvRunningCondition = service.GetDataVolumeCondition(DVRunningConditionType, dv.Status.Conditions) - } - switch { case IsImageProvisioningFinished(condition): log.Info("Disk provisioning finished: clean up") setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, supgen, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - // Unprotect upload time supplements to delete them later. err = ds.uploaderService.Unprotect(ctx, supgen, pod, svc, ing) if err != nil { return reconcile.Result{}, err } - err = ds.diskService.Unprotect(ctx, supgen, dv) + err = ds.diskService.Unprotect(ctx, supgen) if err != nil { return reconcile.Result{}, err } return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(pod, svc, ing, dv, pvc): + case object.AnyTerminating(pod, svc, ing, pvc): log.Info("Waiting for supplements to be terminated") case pod == nil || svc == nil || ing == nil: ds.recorder.Event( @@ -180,12 +161,17 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI case !podutil.IsPodComplete(pod): log.Info("Provisioning to DVCR is in progress", "podPhase", pod.Status.Phase) + uploadStarted := ds.statService.IsUploadStarted(vi.GetUID(), pod) || hasUploadProgress(vi.Status.Progress) err = ds.statService.CheckPod(pod) if err != nil { + if isTransientPodError(err) && uploadStarted { + setUploadProvisioningPhaseCondition(cb, vi) + return reconcile.Result{RequeueAfter: time.Second}, nil + } return reconcile.Result{}, setPhaseConditionFromPodError(cb, vi, err) } - if !ds.statService.IsUploadStarted(vi.GetUID(), pod) { + if !uploadStarted { if isUploaderReady { log.Info("Waiting for the user upload", "pod.phase", pod.Status.Phase) @@ -202,10 +188,10 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI } else { log.Info("Waiting for the uploader to be ready to process the user's upload", "pod.phase", pod.Status.Phase) - vi.Status.Phase = v1alpha2.ImagePending + vi.Status.Phase = v1alpha2.ImageProvisioning cb. Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningNotStarted). + Reason(vicondition.Provisioning). Message(fmt.Sprintf("Waiting for the uploader %q to be ready to process the user's upload.", pod.Name)) } @@ -225,126 +211,15 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI if err != nil { return reconcile.Result{}, err } - case dv == nil: + default: ds.recorder.Event( vi, corev1.EventTypeNormal, v1alpha2.ReasonDataSourceSyncStarted, "The Upload DataSource import to PVC has started", ) - - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = v1alpha2.ImageFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - vi.Status.Progress = "50.0%" - vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) - - var diskSize resource.Quantity - diskSize, err = ds.getPVCSize(pod) - if err != nil { - setPhaseConditionToFailed(cb, &vi.Status.Phase, err) - - if errors.Is(err, service.ErrInsufficientPVCSize) { - return reconcile.Result{}, nil - } - - return reconcile.Result{}, err - } - source := ds.getSource(supgen, ds.statService.GetDVCRImageName(pod)) - - var sc *storagev1.StorageClass - sc, err = ds.diskService.GetStorageClass(ctx, vi.Status.StorageClassName) - if err != nil { - return reconcile.Result{}, err - } - err = ds.diskService.StartImmediate(ctx, diskSize, sc, source, vi, supgen) - if updated, err := setPhaseConditionFromStorageError(err, vi, cb); err != nil || updated { - return reconcile.Result{}, err - } - - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC Provisioner not found: create the new one.") - - return reconcile.Result{RequeueAfter: time.Second}, nil - case dvQuotaNotExceededCondition != nil && dvQuotaNotExceededCondition.Status == corev1.ConditionFalse: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.QuotaExceeded). - Message(dvQuotaNotExceededCondition.Message) - return reconcile.Result{}, nil - case dvRunningCondition != nil && dvRunningCondition.Status != corev1.ConditionTrue && dvRunningCondition.Reason == DVImagePullFailedReason: - vi.Status.Phase = v1alpha2.ImagePending - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ImagePullFailed). - Message(dvRunningCondition.Message) - ds.recorder.Event(vi, corev1.EventTypeWarning, vicondition.ImagePullFailed.String(), dvRunningCondition.Message) - return reconcile.Result{}, nil - case pvc == nil: - vi.Status.Phase = v1alpha2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC not found: waiting for creation.") - return reconcile.Result{RequeueAfter: time.Second}, nil - case ds.diskService.IsImportDone(dv, pvc): - log.Info("Import has completed", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - v1alpha2.ReasonDataSourceSyncCompleted, - "The Upload DataSource import has completed", - ) - - vi.Status.Phase = v1alpha2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Progress = "100%" - vi.Status.Size = ds.statService.GetSize(pod) - vi.Status.CDROM = ds.statService.GetCDROM(pod) - vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - log.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "dv.phase", dv.Status.Phase) - default: - log.Info("Provisioning to PVC is in progress", "dvProgress", dv.Status.Progress, "dvPhase", dv.Status.Phase, "pvcPhase", pvc.Status.Phase) - - vi.Status.Progress = ds.diskService.GetProgress(dv, vi.Status.Progress, service.NewScaleOption(50, 100)) - vi.Status.Target.PersistentVolumeClaim = dv.Status.ClaimName - - err = ds.diskService.Protect(ctx, supgen, vi, dv, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = setPhaseConditionForPVCProvisioningImage(ctx, dv, vi, pvc, cb, ds.diskService) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil + return reconcilePVCImportFromDVCR(ctx, vi, pod, pvc, source, cb, supgen, ds.statService, ds.diskService) } return reconcile.Result{RequeueAfter: time.Second}, nil @@ -399,7 +274,7 @@ func (ds UploadDataSource) StoreToDVCR(ctx context.Context, vi *v1alpha2.Virtual return CleanUpSupplements(ctx, vi, ds) case object.AnyTerminating(pod, svc, ing): - vi.Status.Phase = v1alpha2.ImagePending + vi.Status.Phase = v1alpha2.ImageProvisioning log.Info("Cleaning up...") case pod == nil || svc == nil || ing == nil: @@ -456,9 +331,13 @@ func (ds UploadDataSource) StoreToDVCR(ctx context.Context, vi *v1alpha2.Virtual vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) log.Info("Ready", "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) - case ds.statService.IsUploadStarted(vi.GetUID(), pod): + case ds.statService.IsUploadStarted(vi.GetUID(), pod) || hasUploadProgress(vi.Status.Progress): err = ds.statService.CheckPod(pod) if err != nil { + if isTransientPodError(err) { + setUploadProvisioningPhaseCondition(cb, vi) + return reconcile.Result{RequeueAfter: time.Second}, nil + } return reconcile.Result{}, setPhaseConditionFromPodError(cb, vi, err) } @@ -495,10 +374,10 @@ func (ds UploadDataSource) StoreToDVCR(ctx context.Context, vi *v1alpha2.Virtual default: cb. Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningNotStarted). + Reason(vicondition.Provisioning). Message(fmt.Sprintf("Waiting for the uploader %q to be ready to process the user's upload.", pod.Name)) - vi.Status.Phase = v1alpha2.ImagePending + vi.Status.Phase = v1alpha2.ImageProvisioning log.Info("Waiting for the uploader to be ready to process the user's upload", "pod.phase", pod.Status.Phase) } @@ -526,6 +405,27 @@ func (ds UploadDataSource) Validate(_ context.Context, _ *v1alpha2.VirtualImage) return nil } +func setUploadProvisioningPhaseCondition(cb *conditions.ConditionBuilder, vi *v1alpha2.VirtualImage) { + vi.Status.Phase = v1alpha2.ImageProvisioning + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Import is in the process of provisioning to DVCR.") +} + +func isTransientPodError(err error) bool { + return errors.Is(err, service.ErrNotInitialized) || errors.Is(err, service.ErrNotScheduled) +} + +func hasUploadProgress(progress string) bool { + switch progress { + case "", "0", "0%", "0.0%", "0.00%": + return false + default: + return true + } +} + func (ds UploadDataSource) getEnvSettings(vi *v1alpha2.VirtualImage, supgen supplements.Generator) *uploader.Settings { var settings uploader.Settings @@ -573,7 +473,7 @@ func (ds UploadDataSource) getPVCSize(pod *corev1.Pod) (resource.Quantity, error return service.GetValidatedPVCSize(&unpackedSize, unpackedSize) } -func (ds UploadDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *cdiv1.DataVolumeSource { +func (ds UploadDataSource) getSource(sup supplements.Generator, dvcrSourceImageName string) *service.PVCImportSource { // The image was preloaded from source into dvcr. // We can't use the same data source a second time, but we can set dvcr as the data source. // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. @@ -581,11 +481,5 @@ func (ds UploadDataSource) getSource(sup supplements.Generator, dvcrSourceImageN secretName := sup.DVCRAuthSecretForDV().Name certConfigMapName := sup.DVCRCABundleConfigMapForDV().Name - return &cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: &url, - SecretRef: &secretName, - CertConfigMap: &certConfigMapName, - }, - } + return service.NewPVCRegistryImportSource(url, secretName, certConfigMapName) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/datavolume_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/datavolume_watcher.go deleted file mode 100644 index acee82439e..0000000000 --- a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/datavolume_watcher.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2025 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package watcher - -import ( - "fmt" - - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type DataVolumeWatcher struct{} - -func NewDataVolumeWatcher() *DataVolumeWatcher { - return &DataVolumeWatcher{} -} - -func (w *DataVolumeWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &cdiv1.DataVolume{}, - handler.TypedEnqueueRequestForOwner[*cdiv1.DataVolume]( - mgr.GetScheme(), - mgr.GetRESTMapper(), - &v1alpha2.VirtualImage{}, - handler.OnlyControllerOwner(), - ), - predicate.TypedFuncs[*cdiv1.DataVolume]{ - CreateFunc: func(e event.TypedCreateEvent[*cdiv1.DataVolume]) bool { return false }, - UpdateFunc: func(e event.TypedUpdateEvent[*cdiv1.DataVolume]) bool { - if e.ObjectOld.Status.Progress != e.ObjectNew.Status.Progress { - return true - } - - if e.ObjectOld.Status.Phase != e.ObjectNew.Status.Phase { - switch e.ObjectNew.Status.Phase { - case cdiv1.Succeeded, cdiv1.WaitForFirstConsumer, cdiv1.PendingPopulation: - return true - } - } - - if e.ObjectOld.Status.ClaimName != e.ObjectNew.Status.ClaimName { - return true - } - - oldDVQuotaNotExceeded, oldOk := conditions.GetDataVolumeCondition(conditions.DVQoutaNotExceededConditionType, e.ObjectOld.Status.Conditions) - newDVQuotaNotExceeded, newOk := conditions.GetDataVolumeCondition(conditions.DVQoutaNotExceededConditionType, e.ObjectNew.Status.Conditions) - - if !oldOk && newOk { - return true - } - - if oldOk && newOk && oldDVQuotaNotExceeded != newDVQuotaNotExceeded { - return true - } - - dvRunning := service.GetDataVolumeCondition(cdiv1.DataVolumeRunning, e.ObjectNew.Status.Conditions) - return dvRunning != nil && dvRunning.Reason == "Error" - }, - DeleteFunc: func(e event.TypedDeleteEvent[*cdiv1.DataVolume]) bool { return false }, - }, - ), - ); err != nil { - return fmt.Errorf("error setting watch on DV: %w", err) - } - return nil -} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go index 7a2adede14..fff81ee255 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go @@ -62,6 +62,7 @@ func NewController( mgr manager.Manager, log *log.Logger, importerImage string, + diskImporterImage string, uploaderImage string, bounderImage string, requirements corev1.ResourceRequirements, @@ -73,7 +74,12 @@ func NewController( importer := service.NewImporterService(dvcr, mgr.GetClient(), importerImage, requirements, PodPullPolicy, PodVerbose, ControllerName, protection) uploader := service.NewUploaderService(dvcr, mgr.GetClient(), uploaderImage, requirements, PodPullPolicy, PodVerbose, ControllerName, protection) bounder := service.NewBounderPodService(dvcr, mgr.GetClient(), bounderImage, requirements, PodPullPolicy, PodVerbose, ControllerName, protection) - disk := service.NewDiskService(mgr.GetClient(), dvcr, protection, ControllerName) + disk := service.NewDiskService(mgr.GetClient(), dvcr, protection, ControllerName, service.DiskImporterConfig{ + Image: diskImporterImage, + ResourceRequirements: requirements, + PullPolicy: PodPullPolicy, + Verbose: PodVerbose, + }) scService := intsvc.NewVirtualImageStorageClassService(service.NewBaseStorageClassService(mgr.GetClient()), storageClassSettings) dvcrService := service.NewDVCRService(mgr.GetClient()) recorder := eventrecord.NewEventRecorderLogger(mgr, ControllerName) diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go index 739538b649..8f8741e09e 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -116,7 +116,6 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr watcher.NewStorageClassWatcher(mgrClient), watcher.NewVirtualMachineWatcher(mgrClient), watcher.NewVirtualDiskSnapshotWatcher(mgrClient), - watcher.NewDataVolumeWatcher(), watcher.NewPersistentVolumeClaimWatcher(), watcher.NewVirtualDiskWatcher(mgrClient), postponeimporter.NewWatcher[*v1alpha2.VirtualImage](mgrClient, logger), diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/util.go b/images/virtualization-artifact/pkg/controller/vm/internal/util.go index 9bd9ef9639..e30176d587 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/util.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/util.go @@ -110,8 +110,7 @@ var mapPhases = map[virtv1.VirtualMachinePrintableStatus]PhaseGetter{ return v1alpha2.MachineStopped }, - // VirtualMachineStatusProvisioning indicates that cluster resources associated with the virtual machine - // (e.g., DataVolumes) are being provisioned and prepared. + // VirtualMachineStatusProvisioning indicates that cluster resources associated with the virtual machine are being provisioned and prepared. virtv1.VirtualMachineStatusProvisioning: func(_ *v1alpha2.VirtualMachine, _ *virtv1.VirtualMachine) v1alpha2.MachinePhase { return v1alpha2.MachineStarting }, @@ -137,8 +136,7 @@ var mapPhases = map[virtv1.VirtualMachinePrintableStatus]PhaseGetter{ virtv1.VirtualMachineStatusStopping: func(_ *v1alpha2.VirtualMachine, _ *virtv1.VirtualMachine) v1alpha2.MachinePhase { return v1alpha2.MachineStopping }, - // VirtualMachineStatusTerminating indicates that the virtual machine is in the process of deletion, - // as well as its associated resources (VirtualMachineInstance, DataVolumes, …). + // VirtualMachineStatusTerminating indicates that the virtual machine and its associated resources are being deleted. virtv1.VirtualMachineStatusTerminating: func(_ *v1alpha2.VirtualMachine, _ *virtv1.VirtualMachine) v1alpha2.MachinePhase { return v1alpha2.MachineTerminating }, @@ -175,8 +173,7 @@ var mapPhases = map[virtv1.VirtualMachinePrintableStatus]PhaseGetter{ virtv1.VirtualMachineStatusPvcNotFound: func(_ *v1alpha2.VirtualMachine, _ *virtv1.VirtualMachine) v1alpha2.MachinePhase { return v1alpha2.MachinePending }, - // VirtualMachineStatusDataVolumeError indicates that an error has been reported by one of the DataVolumes - // referenced by the virtual machines. + // KubeVirt keeps this status reason for compatibility with older volume flows. virtv1.VirtualMachineStatusDataVolumeError: func(_ *v1alpha2.VirtualMachine, _ *virtv1.VirtualMachine) v1alpha2.MachinePhase { return v1alpha2.MachinePending }, diff --git a/monitoring/prometheus-rules/internal-virtualization-cdi-apiservier.yaml b/monitoring/prometheus-rules/internal-virtualization-cdi-apiservier.yaml deleted file mode 100644 index b93c7fdb2c..0000000000 --- a/monitoring/prometheus-rules/internal-virtualization-cdi-apiservier.yaml +++ /dev/null @@ -1,36 +0,0 @@ -- name: kubernetes.internal.virtualization.cdi_apiserver_state - rules: - - alert: D8InternalVirtualizationCDIAPIServerPodIsNotReady - expr: min by (pod) (kube_pod_status_ready{condition="true", namespace="d8-virtualization", pod=~"cdi-apiserver-.*"}) != 1 - labels: - severity_level: "6" - tier: cluster - for: 10m - annotations: - plk_protocol_version: "1" - plk_markup_format: "markdown" - plk_labels_as_annotations: "pod" - plk_create_group_if_not_exists__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - plk_grouped_by__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - summary: The cdi-apiserver Pod is NOT Ready. - description: | - The recommended course of action: - 1. Retrieve details of the Deployment: `kubectl -n d8-virtualization describe deploy cdi-apiserver` - 2. View the status of the Pod and try to figure out why it is not running: `kubectl -n d8-virtualization describe pod -l cdi.internal.virtualization.deckhouse.io=cdi-apiserver` - - - alert: D8InternalVirtualizationCDIAPIServerPodIsNotRunning - expr: absent(kube_pod_status_phase{namespace="d8-virtualization",phase="Running",pod=~"cdi-apiserver-.*"}) - labels: - severity_level: "6" - tier: cluster - for: 10m - annotations: - plk_protocol_version: "1" - plk_markup_format: "markdown" - plk_create_group_if_not_exists__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - plk_grouped_by__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - summary: The cdi-apiserver Pod is NOT Running. - description: | - The recommended course of action: - 1. Retrieve details of the Deployment: `kubectl -n d8-virtualization describe deploy cdi-apiserver` - 2. View the status of the Pod and try to figure out why it is not running: `kubectl -n d8-virtualization describe pod -l cdi.internal.virtualization.deckhouse.io=cdi-apiserver` diff --git a/monitoring/prometheus-rules/internal-virtualization-cdi-deployment.yaml b/monitoring/prometheus-rules/internal-virtualization-cdi-deployment.yaml deleted file mode 100644 index 671ac2ec33..0000000000 --- a/monitoring/prometheus-rules/internal-virtualization-cdi-deployment.yaml +++ /dev/null @@ -1,36 +0,0 @@ -- name: kubernetes.internal.virtualization.cdi_deployment_state - rules: - - alert: D8InternalVirtualizationCDIDeploymentPodIsNotReady - expr: min by (pod) (kube_pod_status_ready{condition="true", namespace="d8-virtualization", pod=~"cdi-deployment-.*"}) != 1 - labels: - severity_level: "6" - tier: cluster - for: 10m - annotations: - plk_protocol_version: "1" - plk_markup_format: "markdown" - plk_labels_as_annotations: "pod" - plk_create_group_if_not_exists__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - plk_grouped_by__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - summary: The cdi-deployment Pod is NOT Ready. - description: | - The recommended course of action: - 1. Retrieve details of the Deployment: `kubectl -n d8-virtualization describe deploy cdi-deployment` - 2. View the status of the Pod and try to figure out why it is not running: `kubectl -n d8-virtualization describe pod -l app=containerized-data-importer` - - - alert: D8InternalVirtualizationCDIDeploymentPodIsNotRunning - expr: absent(kube_pod_status_phase{namespace="d8-virtualization",phase="Running",pod=~"cdi-deployment-.*"}) - labels: - severity_level: "6" - tier: cluster - for: 10m - annotations: - plk_protocol_version: "1" - plk_markup_format: "markdown" - plk_create_group_if_not_exists__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - plk_grouped_by__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - summary: The cdi-deployment Pod is NOT Running. - description: | - The recommended course of action: - 1. Retrieve details of the Deployment: `kubectl -n d8-virtualization describe deploy cdi-deployment` - 2. View the status of the Pod and try to figure out why it is not running: `kubectl -n d8-virtualization describe pod -l app=containerized-data-importer` diff --git a/monitoring/prometheus-rules/internal-virtualization-cdi-operator.yaml b/monitoring/prometheus-rules/internal-virtualization-cdi-operator.yaml deleted file mode 100644 index eb5f20decc..0000000000 --- a/monitoring/prometheus-rules/internal-virtualization-cdi-operator.yaml +++ /dev/null @@ -1,36 +0,0 @@ -- name: kubernetes.internal.virtualization.cdi_operator_state - rules: - - alert: D8InternalVirtualizationCDIOperatorPodIsNotReady - expr: min by (pod) (kube_pod_status_ready{condition="true", namespace="d8-virtualization", pod=~"cdi-operator-.*"}) != 1 - labels: - severity_level: "6" - tier: cluster - for: 10m - annotations: - plk_protocol_version: "1" - plk_markup_format: "markdown" - plk_labels_as_annotations: "pod" - plk_create_group_if_not_exists__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - plk_grouped_by__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - summary: The cdi-operator Pod is NOT Ready. - description: | - The recommended course of action: - 1. Retrieve details of the Deployment: `kubectl -n d8-virtualization describe deploy cdi-operator` - 2. View the status of the Pod and try to figure out why it is not running: `kubectl -n d8-virtualization describe pod -l app=cdi-operator` - - - alert: D8InternalVirtualizationCDIOperatorPodIsNotRunning - expr: absent(kube_pod_status_phase{namespace="d8-virtualization",phase="Running",pod=~"cdi-operator-.*"}) - labels: - severity_level: "6" - tier: cluster - for: 10m - annotations: - plk_protocol_version: "1" - plk_markup_format: "markdown" - plk_create_group_if_not_exists__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - plk_grouped_by__d8_internal_virtualization_cdi_health: "D8InternalVirtualizationCDIHealth,tier=~tier,prometheus=deckhouse,kubernetes=~kubernetes" - summary: The cdi-operator Pod is NOT Running. - description: | - The recommended course of action: - 1. Retrieve details of the Deployment: `kubectl -n d8-virtualization describe deploy cdi-operator` - 2. View the status of the Pod and try to figure out why it is not running: `kubectl -n d8-virtualization describe pod -l app=cdi-operator` diff --git a/src/kubevirt-rules/kubevirt_rules.go b/src/kubevirt-rules/kubevirt_rules.go index a4f2520993..730e13804b 100644 --- a/src/kubevirt-rules/kubevirt_rules.go +++ b/src/kubevirt-rules/kubevirt_rules.go @@ -30,27 +30,17 @@ var KubevirtRewriteRules = &rewriter.RewriteRules{ KindPrefix: "InternalVirtualization", // VirtualMachine -> InternalVirtualizationVirtualMachine ResourceTypePrefix: "internalvirtualization", // virtualmachines -> internalvirtualizationvirtualmachines ShortNamePrefix: "intvirt", // kubectl get intvirtvm - Categories: []string{"intvirt"}, // kubectl get intvirt to see all KubeVirt and CDI resources. + Categories: []string{"intvirt"}, // kubectl get intvirt to see all internal virtualization resources. Rules: KubevirtAPIGroupsRules, Webhooks: KubevirtWebhooks, Labels: rewriter.MetadataReplace{ Names: []rewriter.MetadataReplaceRule{ - {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, {Original: "operator.kubevirt.io", Renamed: "operator.kubevirt." + internalPrefix}, {Original: "prometheus.kubevirt.io", Renamed: "prometheus.kubevirt." + internalPrefix}, - {Original: "prometheus.cdi.kubevirt.io", Renamed: "prometheus.cdi." + internalPrefix}, // Special cases. {Original: "node-labeller.kubevirt.io/skip-node", Renamed: "node-labeller." + rootPrefix + "/skip-node"}, {Original: "node-labeller.kubevirt.io/obsolete-host-model", Renamed: "node-labeller." + internalPrefix + "/obsolete-host-model"}, - { - Original: "app.kubernetes.io/managed-by", OriginalValue: "cdi-operator", - Renamed: "app.kubernetes.io/managed-by", RenamedValue: "cdi-operator-internal-virtualization", - }, - { - Original: "app.kubernetes.io/managed-by", OriginalValue: "cdi-controller", - Renamed: "app.kubernetes.io/managed-by", RenamedValue: "cdi-controller-internal-virtualization", - }, { Original: "app.kubernetes.io/managed-by", OriginalValue: "virt-operator", Renamed: "app.kubernetes.io/managed-by", RenamedValue: "virt-operator-internal-virtualization", @@ -61,11 +51,6 @@ var KubevirtRewriteRules = &rewriter.RewriteRules{ }, }, Prefixes: []rewriter.MetadataReplaceRule{ - // CDI related labels. - {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, - {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, - {Original: "prometheus.cdi.kubevirt.io", Renamed: "prometheus.cdi." + internalPrefix}, - {Original: "upload.cdi.kubevirt.io", Renamed: "upload.cdi." + internalPrefix}, // KubeVirt related labels. {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, {Original: "prometheus.kubevirt.io", Renamed: "prometheus.kubevirt." + internalPrefix}, @@ -87,9 +72,6 @@ var KubevirtRewriteRules = &rewriter.RewriteRules{ }, Annotations: rewriter.MetadataReplace{ Prefixes: []rewriter.MetadataReplaceRule{ - // CDI related annotations. - {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, - {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, // KubeVirt related annotations. {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, {Original: "certificates.kubevirt.io", Renamed: "certificates.kubevirt." + internalPrefix}, @@ -98,32 +80,11 @@ var KubevirtRewriteRules = &rewriter.RewriteRules{ Finalizers: rewriter.MetadataReplace{ Prefixes: []rewriter.MetadataReplaceRule{ {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, - {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, - }, - }, - Excludes: []rewriter.ExcludeRule{ - rewriter.ExcludeRule{ - Kinds: []string{ - "PersistentVolumeClaim", - "PersistentVolume", - "Pod", - }, - MatchLabels: map[string]string{ - "app.kubernetes.io/managed-by": "cdi-controller", - }, - }, - rewriter.ExcludeRule{ - Kinds: []string{ - "CDI", - }, - MatchNames: []string{ - "cdi", - }, }, }, } -// TODO create generator in golang to produce below rules from Kubevirt and CDI sources so proxy can work with future versions. +// TODO create generator in golang to produce below rules from KubeVirt sources so proxy can work with future versions. var KubevirtAPIGroupsRules = map[string]rewriter.APIGroupRule{ "cdi.kubevirt.io": { @@ -134,72 +95,6 @@ var KubevirtAPIGroupsRules = map[string]rewriter.APIGroupRule{ Renamed: "cdi." + internalPrefix, }, ResourceRules: map[string]rewriter.ResourceRule{ - // cdiconfigs.cdi.kubevirt.io - "cdiconfigs": { - Kind: "CDIConfig", - ListKind: "CDIConfigList", - Plural: "cdiconfigs", - Singular: "cdiconfig", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // cdis.cdi.kubevirt.io - "cdis": { - Kind: "CDI", - ListKind: "CDIList", - Plural: "cdis", - Singular: "cdi", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{"cdi", "cdis"}, - }, - // dataimportcrons.cdi.kubevirt.io - "dataimportcrons": { - Kind: "DataImportCron", - ListKind: "DataImportCronList", - Plural: "dataimportcrons", - Singular: "dataimportcron", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{"all"}, - ShortNames: []string{"dic", "dics"}, - }, - // datasources.cdi.kubevirt.io - "datasources": { - Kind: "DataSource", - ListKind: "DataSourceList", - Plural: "datasources", - Singular: "datasource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{"all"}, - ShortNames: []string{"das"}, - }, - // datavolumes.cdi.kubevirt.io - "datavolumes": { - Kind: "DataVolume", - ListKind: "DataVolumeList", - Plural: "datavolumes", - Singular: "datavolume", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{"all"}, - ShortNames: []string{"dv", "dvs"}, - }, - // objecttransfers.cdi.kubevirt.io - "objecttransfers": { - Kind: "ObjectTransfer", - ListKind: "ObjectTransferList", - Plural: "objecttransfers", - Singular: "objecttransfer", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{"ot", "ots"}, - }, // storageprofiles.cdi.kubevirt.io "storageprofiles": { Kind: "StorageProfile", @@ -211,69 +106,6 @@ var KubevirtAPIGroupsRules = map[string]rewriter.APIGroupRule{ Categories: []string{}, ShortNames: []string{}, }, - // volumeclonesources.cdi.kubevirt.io - "volumeclonesources": { - Kind: "VolumeCloneSource", - ListKind: "VolumeCloneSourceList", - Plural: "volumeclonesources", - Singular: "volumeclonesource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // volumeimportsources.cdi.kubevirt.io - "volumeimportsources": { - Kind: "VolumeImportSource", - ListKind: "VolumeImportSourceList", - Plural: "volumeimportsources", - Singular: "volumeimportsource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // volumeuploadsources.cdi.kubevirt.io - "volumeuploadsources": { - Kind: "VolumeUploadSource", - ListKind: "VolumeUploadSourceList", - Plural: "volumeuploadsources", - Singular: "volumeuploadsource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - }, - }, - "forklift.cdi.kubevirt.io": { - GroupRule: rewriter.GroupRule{ - Group: "forklift.cdi.kubevirt.io", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Renamed: "forklift.cdi." + internalPrefix, - }, - ResourceRules: map[string]rewriter.ResourceRule{ - // openstackvolumepopulators.forklift.cdi.kubevirt.io - "openstackvolumepopulators": { - Kind: "OpenstackVolumePopulator", - ListKind: "OpenstackVolumePopulatorList", - Plural: "openstackvolumepopulators", - Singular: "openstackvolumepopulator", - ShortNames: []string{"osvp", "osvps"}, - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - }, - // ovirtvolumepopulators.forklift.cdi.kubevirt.io - "ovirtvolumepopulators": { - Kind: "OvirtVolumePopulator", - ListKind: "OvirtVolumePopulatorList", - Plural: "ovirtvolumepopulators", - Singular: "ovirtvolumepopulator", - ShortNames: []string{"ovvp", "ovvps"}, - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - }, }, }, "kubevirt.io": { @@ -281,7 +113,7 @@ var KubevirtAPIGroupsRules = map[string]rewriter.APIGroupRule{ Group: "kubevirt.io", Versions: []string{"v1", "v1alpha3"}, PreferredVersion: "v1", - Renamed: "internal.virtualization.deckhouse.io", + Renamed: internalPrefix, }, ResourceRules: map[string]rewriter.ResourceRule{ // kubevirts.kubevirt.io @@ -536,41 +368,6 @@ var KubevirtAPIGroupsRules = map[string]rewriter.APIGroupRule{ } var KubevirtWebhooks = map[string]rewriter.WebhookRule{ - // CDI webhooks. - // Run this in original CDI installation: - // kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -l cdi.kubevirt.io -o json | jq '.items[] | .webhooks[] | {"path": .clientConfig.service.path, "group": (.rules[]|.apiGroups|join(",")), "resource": (.rules[]|.resources|join(",")) } | "\""+.path +"\": {\nPath: \"" + .path + "\",\nGroup: \"" + .group + "\",\nResource: \"" + .resource + "\",\n}," ' -r - // TODO create generator in golang to extract these rules from resource definitions in the cdi-operator package. - "/datavolume-mutate": { - Path: "/datavolume-mutate", - Group: "cdi.kubevirt.io", - Resource: "datavolumes", - }, - "/dataimportcron-validate": { - Path: "/dataimportcron-validate", - Group: "cdi.kubevirt.io", - Resource: "dataimportcrons", - }, - "/datavolume-validate": { - Path: "/datavolume-validate", - Group: "cdi.kubevirt.io", - Resource: "datavolumes", - }, - "/cdi-validate": { - Path: "/cdi-validate", - Group: "cdi.kubevirt.io", - Resource: "cdis", - }, - "/objecttransfer-validate": { - Path: "/objecttransfer-validate", - Group: "cdi.kubevirt.io", - Resource: "objecttransfers", - }, - "/populator-validate": { - Path: "/populator-validate", - Group: "cdi.kubevirt.io", - Resource: "volumeimportsources", // Also, volumeuploadsources. This field for logging only. - }, - // Kubevirt webhooks. // Run this in original Kubevirt installation: // kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -l kubevirt.io -o json | jq '.items[] | .webhooks[] | {"path": .clientConfig.service.path, "group": (.rules[]|.apiGroups|join(",")), "resource": (.rules[]|.resources|join(",")) } | "\""+.path +"\": {\nPath: \"" + .path + "\",\nGroup: \"" + .group + "\",\nResource: \"" + .resource + "\",\n}," ' diff --git a/templates/admission-policy.yaml b/templates/admission-policy.yaml index 029d7ac860..070ac4b3f3 100644 --- a/templates/admission-policy.yaml +++ b/templates/admission-policy.yaml @@ -19,10 +19,8 @@ spec: matchConstraints: resourceRules: - apiGroups: - - "cdi.internal.virtualization.deckhouse.io" - "clone.internal.virtualization.deckhouse.io" - "export.internal.virtualization.deckhouse.io" - - "forklift.cdi.internal.virtualization.deckhouse.io" - "instancetype.internal.virtualization.deckhouse.io" - "internal.virtualization.deckhouse.io" - "pool.internal.virtualization.deckhouse.io" @@ -40,11 +38,6 @@ spec: request.userInfo.username.startsWith("system:serviceaccount:kube-system:") || request.userInfo.username.startsWith("system:serviceaccount:d8-system:") || request.userInfo.username in [ - "system:serviceaccount:d8-virtualization:cdi-apiserver", - "system:serviceaccount:d8-virtualization:cdi-cronjob", - "system:serviceaccount:d8-virtualization:cdi-operator", - "system:serviceaccount:d8-virtualization:cdi-sa", - "system:serviceaccount:d8-virtualization:cdi-uploadproxy", "system:serviceaccount:d8-virtualization:kubevirt-internal-virtualization-apiserver", "system:serviceaccount:d8-virtualization:kubevirt-internal-virtualization-controller", "system:serviceaccount:d8-virtualization:kubevirt-internal-virtualization-exportproxy", diff --git a/templates/cdi/cdi-apiserver/vpa.yaml b/templates/cdi/cdi-apiserver/vpa.yaml deleted file mode 100644 index 4a0c3adcd3..0000000000 --- a/templates/cdi/cdi-apiserver/vpa.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} ---- -apiVersion: autoscaling.k8s.io/v1 -kind: VerticalPodAutoscaler -metadata: - name: cdi-apiserver - namespace: d8-{{ .Chart.Name }} - {{- include "helm_lib_module_labels" (list . (dict "app" "cdi-apiserver" )) | nindent 2 }} -spec: - targetRef: - apiVersion: "apps/v1" - kind: Deployment - name: cdi-apiserver - updatePolicy: - updateMode: {{ include "vpa.policyUpdateMode" . }} - resourcePolicy: - containerPolicies: - {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} - - containerName: cdi-apiserver - minAllowed: - cpu: 10m - memory: 20Mi - maxAllowed: - cpu: 100m - memory: 40Mi -{{- end }} diff --git a/templates/cdi/cdi-deployment/rbac-for-us.yaml b/templates/cdi/cdi-deployment/rbac-for-us.yaml deleted file mode 100644 index 18fcb00d18..0000000000 --- a/templates/cdi/cdi-deployment/rbac-for-us.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: d8:{{ .Chart.Name }}:cdi-deployment-rbac-proxy - {{- include "helm_lib_module_labels" (list . (dict "app" "cdi-deployment")) | nindent 2 }} -subjects: - - kind: ServiceAccount - name: cdi-sa - namespace: d8-{{ .Chart.Name }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: d8:rbac-proxy diff --git a/templates/cdi/cdi-deployment/vpa.yaml b/templates/cdi/cdi-deployment/vpa.yaml deleted file mode 100644 index ed1f167b88..0000000000 --- a/templates/cdi/cdi-deployment/vpa.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} ---- -apiVersion: autoscaling.k8s.io/v1 -kind: VerticalPodAutoscaler -metadata: - name: cdi-deployment - namespace: d8-{{ .Chart.Name }} - {{- include "helm_lib_module_labels" (list . (dict "app" "cdi-deployment" )) | nindent 2 }} -spec: - targetRef: - apiVersion: "apps/v1" - kind: Deployment - name: cdi-deployment - updatePolicy: - updateMode: {{ include "vpa.policyUpdateMode" . }} - resourcePolicy: - containerPolicies: - {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} - - containerName: cdi-deployment - minAllowed: - cpu: 10m - memory: 30Mi - maxAllowed: - cpu: 100m - memory: 60Mi -{{- end }} diff --git a/templates/cdi/cdi-operator/configmap.yaml b/templates/cdi/cdi-operator/configmap.yaml deleted file mode 100644 index 354fea7412..0000000000 --- a/templates/cdi/cdi-operator/configmap.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: cdi-operator-leader-election-helper - namespace: d8-{{ .Chart.Name }} - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} diff --git a/templates/cdi/cdi-operator/deployment.yaml b/templates/cdi/cdi-operator/deployment.yaml deleted file mode 100644 index 40e2dad3f3..0000000000 --- a/templates/cdi/cdi-operator/deployment.yaml +++ /dev/null @@ -1,143 +0,0 @@ -{{- $priorityClassName := include "priorityClassName" . }} - -{{- define "cdi_images" -}} -- name: CONTROLLER_IMAGE - value: {{ include "helm_lib_module_image" (list . "cdiController") }} -- name: IMPORTER_IMAGE - value: {{ include "helm_lib_module_image" (list . "cdiImporter") }} -- name: CLONER_IMAGE - value: {{ include "helm_lib_module_image" (list . "cdiCloner") }} -- name: OVIRT_POPULATOR_IMAGE - value: {{ include "helm_lib_module_image" (list . "cdiImporter") }} -- name: APISERVER_IMAGE - value: {{ include "helm_lib_module_image" (list . "cdiApiserver") }} -{{- end }} - -{{- define "cdi_operator_resources" }} -cpu: 15m -memory: 15Mi -{{- end }} - -{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} ---- -apiVersion: autoscaling.k8s.io/v1 -kind: VerticalPodAutoscaler -metadata: - name: cdi-operator - namespace: d8-{{ .Chart.Name }} - {{- include "helm_lib_module_labels" (list . (dict "app" "cdi-operator" )) | nindent 2 }} -spec: - targetRef: - apiVersion: "apps/v1" - kind: Deployment - name: cdi-operator - updatePolicy: - updateMode: {{ include "vpa.policyUpdateMode" . }} - resourcePolicy: - containerPolicies: - {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} - {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} - - containerName: cdi-operator - minAllowed: - {{- include "cdi_operator_resources" . | nindent 8 }} - maxAllowed: - cpu: 20m - memory: 30Mi -{{- end }} ---- -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: cdi-operator - namespace: d8-{{ .Chart.Name }} - {{- include "helm_lib_module_labels" (list . (dict "app" "cdi-operator" )) | nindent 2 }} -spec: - minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} - selector: - matchLabels: - app: cdi-operator ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} - name: cdi-operator - namespace: d8-{{ .Chart.Name }} -spec: - {{- include "helm_lib_deployment_strategy_and_replicas_for_ha" . | nindent 2 }} - revisionHistoryLimit: 2 - selector: - matchLabels: - app: cdi-operator - template: - metadata: - labels: - app: cdi-operator - annotations: - kubectl.kubernetes.io/default-container: cdi-operator - spec: - {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "cdi-operator")) | nindent 6 }} - containers: - {{- include "kube_api_rewriter.sidecar_container" . | nindent 6 }} - {{- $kubeRbacProxySettings := dict }} - {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" true }} - {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} - {{- $_ := set $kubeRbacProxySettings "upstreams" (list - (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") - (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") - (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") - ) }} - {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 6 }} - - name: cdi-operator - {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 8 }} - env: - {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 8 }} - {{- include "cdi_images" . | nindent 8 }} - - name: DEPLOY_CLUSTER_RESOURCES - value: "true" - - name: OPERATOR_VERSION - value: {{ include "cdi_images" . | sha256sum | trunc 7 | quote }} - - name: VERBOSITY - value: "1" - - name: PULL_POLICY - value: IfNotPresent - - name: MONITORING_NAMESPACE - args: - - -metrics_address - - :8080 - image: {{ include "helm_lib_module_image" (list . "cdiOperator") }} - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8080 - name: metrics - protocol: TCP - - containerPort: 8081 - name: health - protocol: TCP - livenessProbe: - httpGet: - path: /healthz - port: health - scheme: HTTP - initialDelaySeconds: 10 - readinessProbe: - httpGet: - path: /readyz - port: health - scheme: HTTP - initialDelaySeconds: 10 - resources: - requests: - {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 12 }} - {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} - {{- include "cdi_operator_resources" . | nindent 12 }} - {{- end }} - volumeMounts: - {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 8 }} - {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} - {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} - {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} - {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} - serviceAccountName: cdi-operator - volumes: - {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 6 }} diff --git a/templates/cdi/cdi-operator/rbac-for-us.yaml b/templates/cdi/cdi-operator/rbac-for-us.yaml deleted file mode 100644 index 2f6bc7722b..0000000000 --- a/templates/cdi/cdi-operator/rbac-for-us.yaml +++ /dev/null @@ -1,540 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} - name: d8:containerized-data-importer:cdi-operator -rules: -- apiGroups: - - rbac.authorization.k8s.io - resources: - - clusterrolebindings - - clusterroles - verbs: - - get - - list - - watch - - create - - update - - delete -- apiGroups: - - security.openshift.io - resources: - - securitycontextconstraints - verbs: - - get - - list - - watch - - update - - create -- apiGroups: - - apiextensions.k8s.io - resources: - - customresourcedefinitions - - customresourcedefinitions/status - verbs: - - get - - list - - watch - - create - - update - - delete -- apiGroups: - - cdi.internal.virtualization.deckhouse.io - - upload.cdi.kubevirt.io - resources: - - '*' - verbs: - - '*' -- apiGroups: - - admissionregistration.k8s.io - resources: - - validatingwebhookconfigurations - - mutatingwebhookconfigurations - verbs: - - create - - list - - watch -- apiGroups: - - admissionregistration.k8s.io - resourceNames: - - cdi-internal-virtualization-api-dataimportcron-validate - - cdi-internal-virtualization-api-populator-validate - - cdi-internal-virtualization-api-datavolume-validate - - cdi-internal-virtualization-api-validate - - cdi-internal-virtualization-objecttransfer-api-validate - resources: - - validatingwebhookconfigurations - verbs: - - get - - update - - delete -- apiGroups: - - admissionregistration.k8s.io - resourceNames: - - cdi-internal-virtualization-api-datavolume-mutate - - cdi-internal-virtualization-api-pvc-mutate - resources: - - mutatingwebhookconfigurations - verbs: - - get - - update - - delete -- apiGroups: - - apiregistration.k8s.io - resources: - - apiservices - verbs: - - get - - list - - watch - - create - - update - - delete -- apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - persistentvolumeclaims - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - persistentvolumes - verbs: - - get - - list - - watch -- apiGroups: - - storage.k8s.io - resources: - - storageclasses - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - namespaces - verbs: - - get -- apiGroups: - - snapshot.storage.k8s.io - resources: - - volumesnapshots - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch -- apiGroups: - - "" - resources: - - persistentvolumeclaims - verbs: - - get - - list - - watch - - create - - update - - delete - - deletecollection - - patch -- apiGroups: - - "" - resources: - - persistentvolumes - verbs: - - get - - list - - watch - - update -- apiGroups: - - "" - resources: - - persistentvolumeclaims/finalizers - - pods/finalizers - verbs: - - update -- apiGroups: - - "" - resources: - - pods - - services - verbs: - - get - - list - - watch - - create - - delete -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - create -- apiGroups: - - storage.k8s.io - resources: - - storageclasses - - csidrivers - verbs: - - get - - list - - watch -- apiGroups: - - config.openshift.io - resources: - - proxies - - infrastructures - verbs: - - get - - list - - watch -- apiGroups: - - config.openshift.io - resources: - - clusterversions - verbs: - - get -- apiGroups: - - storage.deckhouse.io - resources: - - replicatedstorageclasses - - replicatedstoragepools - verbs: - - get - - list - - watch -- apiGroups: - - snapshot.storage.k8s.io - resources: - - volumesnapshots - - volumesnapshotclasses - - volumesnapshotcontents - verbs: - - get - - list - - watch - - create - - delete -- apiGroups: - - snapshot.storage.k8s.io - resources: - - volumesnapshots - verbs: - - update - - deletecollection -- apiGroups: - - apiextensions.k8s.io - resources: - - customresourcedefinitions - verbs: - - get - - list - - watch -- apiGroups: - - scheduling.k8s.io - resources: - - priorityclasses - verbs: - - get - - list - - watch -- apiGroups: - - image.openshift.io - resources: - - imagestreams - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - secrets - verbs: - - create -- apiGroups: - - internal.virtualization.deckhouse.io - resources: - - internalvirtualizationvirtualmachines/finalizers - verbs: - - update -- apiGroups: - - forklift.cdi.internal.virtualization.deckhouse.io - resources: - - internalvirtualizationovirtvolumepopulators - - internalvirtualizationopenstackvolumepopulators - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - persistentvolumeclaims - verbs: - - get -- apiGroups: - - "" - resources: - - resourcequotas - verbs: - - get - - list - - watch - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} - name: d8:containerized-data-importer:cdi-operator -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: d8:containerized-data-importer:cdi-operator -subjects: -- kind: ServiceAccount - name: cdi-operator - namespace: d8-{{ .Chart.Name }} ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} - name: cdi-operator - namespace: d8-{{ .Chart.Name }} -imagePullSecrets: -- name: virtualization-module-registry ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} - name: cdi-operator - namespace: d8-{{ .Chart.Name }} -rules: -- apiGroups: - - rbac.authorization.k8s.io - resources: - - rolebindings - - roles - verbs: - - get - - list - - watch - - create - - update - - delete -- apiGroups: - - "" - resources: - - serviceaccounts - - configmaps - - events - - secrets - - services - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - apps - resources: - - deployments - - deployments/finalizers - verbs: - - get - - list - - watch - - create - - update - - delete -- apiGroups: - - route.openshift.io - resources: - - routes - - routes/custom-host - verbs: - - get - - list - - watch - - create - - update -- apiGroups: - - config.openshift.io - resources: - - proxies - verbs: - - get - - list - - watch -- apiGroups: - - monitoring.coreos.com - resources: - - servicemonitors - - prometheusrules - verbs: - - get - - list - - watch - - create - - delete - - update - - patch -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - create - - update -- apiGroups: - - "" - resources: - - secrets - - configmaps - verbs: - - get - - list - - watch - - create -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - delete -- apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch -- apiGroups: - - batch - resources: - - cronjobs - verbs: - - get - - list - - watch - - create - - update - - deletecollection -- apiGroups: - - batch - resources: - - jobs - verbs: - - create - - deletecollection - - list - - watch -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - create - - update -- apiGroups: - - networking.k8s.io - resources: - - ingresses - verbs: - - get - - list - - watch -- apiGroups: - - route.openshift.io - resources: - - routes - verbs: - - get - - list - - watch -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get -- apiGroups: - - "" - resources: - - services - - endpoints - - pods - verbs: - - get - - list - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} - name: cdi-operator - namespace: d8-{{ .Chart.Name }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: cdi-operator -subjects: -- kind: ServiceAccount - name: cdi-operator - namespace: d8-{{ .Chart.Name }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: d8:{{ .Chart.Name }}:cdi-operator-rbac-proxy - {{- include "helm_lib_module_labels" (list . (dict "app" "cdi-operator")) | nindent 2 }} -subjects: - - kind: ServiceAccount - name: cdi-operator - namespace: d8-{{ .Chart.Name }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: d8:rbac-proxy diff --git a/templates/cdi/config.yaml b/templates/cdi/config.yaml deleted file mode 100644 index 661f372f7f..0000000000 --- a/templates/cdi/config.yaml +++ /dev/null @@ -1,170 +0,0 @@ -{{- $nodeSelectorSystem := index (include "helm_lib_node_selector" (tuple . "system") | fromYaml) "nodeSelector" | default (dict) | toJson }} -{{- $nodeSelectorMaster := index (include "helm_lib_node_selector" (tuple . "master") | fromYaml) "nodeSelector" | default (dict) | toJson }} -{{- $tolerationsSystem := index (include "helm_lib_tolerations" (tuple . "system") | fromYaml) "tolerations" | default (list) | toJson }} -{{- $tolerationsAnyNode := index (include "helm_lib_tolerations" (tuple . "any-node") | fromYaml) "tolerations" | default (list) | toJson }} -{{- $priorityClassName := include "priorityClassName" . }} ---- -apiVersion: cdi.internal.virtualization.deckhouse.io/v1beta1 -kind: InternalVirtualizationCDI -metadata: - name: config - namespace: d8-{{ .Chart.Name }} - {{- include "helm_lib_module_labels" (list .) | nindent 2 }} -spec: - imagePullPolicy: IfNotPresent - infra: - {{- include "helm_lib_node_selector" (tuple . "system") | nindent 4 }} - {{- include "helm_lib_tolerations" (tuple . "system") | nindent 4 }} - priorityClass: {{ $priorityClassName }} - config: - podResourceRequirements: - limits: - cpu: 1000m - # Temporarily increase value to workaround OOMKill during importing huge images ~2.9GiB on linux kernels 6.12+. - # TODO: mitigate root cause and return to 600M. - memory: 3600M - requests: - cpu: 100m - memory: 60M - imagePullSecrets: - - name: virtualization-module-registry - featureGates: - - HonorWaitForFirstConsumer - customizeComponents: - flags: - controller: - metrics_address: 127.0.0.1:8080 - patches: - - resourceType: Deployment - resourceName: cdi-apiserver - patch: '[{"op":"replace","path":"/spec/template/spec/nodeSelector","value":{{ $nodeSelectorMaster }}}]' - type: json - - resourceType: Deployment - resourceName: cdi-apiserver - patch: '[{"op":"replace","path":"/spec/template/spec/tolerations","value":{{ $tolerationsAnyNode }}}]' - type: json - - {{- if (include "helm_lib_ha_enabled" .) }} - # HA settings for deploy/cdi-apiserver. - - resourceType: Deployment - resourceName: cdi-apiserver - patch: '[{"op":"replace","path":"/spec/replicas","value":3}]' - type: json - - resourceType: Deployment - resourceName: cdi-apiserver - patch: {{ include "spec_template_spec_antiaffinity_patch" (list "cdi.kubevirt.io" "cdi-apiserver") }} - type: strategic - - resourceType: Deployment - resourceName: cdi-apiserver - patch: {{ include "spec_strategy_rolling_update_patch" . }} - type: strategic - # HA settings for deploy/cdi-deployment. - - resourceType: Deployment - resourceName: cdi-deployment - patch: '[{"op":"replace","path":"/spec/replicas","value":2}]' - type: json - - resourceType: Deployment - resourceName: cdi-deployment - patch: {{ include "spec_template_spec_antiaffinity_patch" (list "cdi.kubevirt.io" "cdi-deployment") }} - type: strategic - - resourceType: Deployment - resourceName: cdi-deployment - patch: {{ include "spec_strategy_rolling_update_patch" . }} - type: strategic - {{- end }} - - # Add kube-api-rewriter as a sidecar container to cdi-apiserver and cdi-deployment. - - {{- $apiRewriterSettings := dict }} - {{- $_ := set $apiRewriterSettings "WEBHOOK_ADDRESS" "https://127.0.0.1:8443" }} - {{- $_ := set $apiRewriterSettings "WEBHOOK_CERT_FILE" "/var/run/certs/cdi-apiserver-server-cert/tls.crt" }} - {{- $_ := set $apiRewriterSettings "WEBHOOK_KEY_FILE" "/var/run/certs/cdi-apiserver-server-cert/tls.key" }} - {{- $_ := set $apiRewriterSettings "webhookCertsVolumeName" "server-cert" }} - {{- $_ := set $apiRewriterSettings "webhookCertsMountPath" "/var/run/certs/cdi-apiserver-server-cert" }} - - resourceType: Deployment - resourceName: cdi-apiserver - patch: {{ include "kube_api_rewriter.pod_spec_strategic_patch_json" (tuple . "cdi-apiserver" $apiRewriterSettings) }} - type: strategic - - - resourceType: Deployment - resourceName: cdi-deployment - patch: {{ include "kube_api_rewriter.pod_spec_strategic_patch_json" (tuple . "cdi-deployment") }} - type: strategic - - # Add kube-rbac-proxy as a sidecar container to cdi-apiserver and cdi-deployment. - {{- $kubeRbacProxySettings := dict }} - {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" true }} - {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} - {{- $_ := set $kubeRbacProxySettings "upstreams" (list - (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") - (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "cdi-operator") - (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") - (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") - ) }} - - resourceType: Deployment - resourceName: cdi-apiserver - patch: {{ include "kube_rbac_proxy.pod_spec_strategic_patch_json" (tuple . $kubeRbacProxySettings) }} - type: strategic - - {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} - {{- $_ := set $kubeRbacProxySettings "upstreams" (list - (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") - (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "cdi-deployment") - (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") - (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") - ) }} - - resourceType: Deployment - resourceName: cdi-deployment - patch: {{ include "kube_rbac_proxy.pod_spec_strategic_patch_json" (tuple . $kubeRbacProxySettings) }} - type: strategic - - resourceType: Service - resourceName: cdi-prometheus-metrics - patch: '[{"op": "replace", "path": "/spec/ports/0/targetPort", "value": "https-metrics"}]' - type: json - - # Add rewriter proxy container port to the Service used by webhook configurations. - # First need to set name for existing port to make strategic patch works later. - - resourceName: cdi-api - resourceType: Service - patch: | - [{"op":"replace", "path":"/spec/ports/0/name", "value":"https"}] - type: json - - resourceName: cdi-api - resourceType: Service - patch: {{ include "kube_api_rewriter.service_spec_port_patch_json" . }} - type: strategic - - # Change service port in webhook configurations. - # NOTE: names are prefixed with "cdi-internal-virtualization-". - - resourceName: cdi-internal-virtualization-api-datavolume-mutate - resourceType: MutatingWebhookConfiguration - patch: {{ include "kube_api_rewriter.webhook_spec_port_patch_json" "datavolume-mutate.cdi.kubevirt.io"}} - type: strategic - - resourceName: cdi-internal-virtualization-api-dataimportcron-validate - resourceType: ValidatingWebhookConfiguration - patch: {{ include "kube_api_rewriter.webhook_spec_port_patch_json" "dataimportcron-validate.cdi.kubevirt.io"}} - type: strategic - - resourceName: cdi-internal-virtualization-api-datavolume-validate - resourceType: ValidatingWebhookConfiguration - patch: {{ include "kube_api_rewriter.webhook_spec_port_patch_json" "datavolume-validate.cdi.kubevirt.io"}} - type: strategic - - resourceName: cdi-internal-virtualization-api-populator-validate - resourceType: ValidatingWebhookConfiguration - patch: {{ include "kube_api_rewriter.webhook_spec_port_patch_json" "populator-validate.cdi.kubevirt.io"}} - type: strategic - - resourceName: cdi-internal-virtualization-api-validate - resourceType: ValidatingWebhookConfiguration - patch: {{ include "kube_api_rewriter.webhook_spec_port_patch_json" "cdi-validate.cdi.kubevirt.io"}} - type: strategic - - resourceName: cdi-internal-virtualization-objecttransfer-api-validate - resourceType: ValidatingWebhookConfiguration - patch: {{ include "kube_api_rewriter.webhook_spec_port_patch_json" "objecttransfer-validate.cdi.kubevirt.io"}} - type: strategic - - workload: - nodeSelector: - kubernetes.io/os: linux - tolerations: - - key: dedicated.deckhouse.io - operator: Equal - value: system diff --git a/templates/cdi/service-monitor.yaml b/templates/cdi/service-monitor.yaml deleted file mode 100644 index 2f48af94b0..0000000000 --- a/templates/cdi/service-monitor.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if (.Values.global.enabledModules | has "operator-prometheus-crd") }} ---- -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: {{ .Chart.Name }}-cdi - namespace: d8-monitoring - {{- include "helm_lib_module_labels" (list $ (dict "prometheus" "main")) | nindent 2 }} -spec: - endpoints: - - bearerTokenSecret: - key: token - name: prometheus-token - path: /metrics - port: metrics - scheme: https - tlsConfig: - insecureSkipVerify: true - metricRelabelings: - # rename kubevirt_cdi_* -> d8_internal_virtualization_kubevirt_cdi_* - - action: replace - regex: kubevirt_cdi_(.*) - replacement: d8_internal_virtualization_kubevirt_cdi_$1 - sourceLabels: - - __name__ - targetLabel: __name__ - namespaceSelector: - matchNames: - - d8-{{ .Chart.Name }} - selector: - matchLabels: - prometheus.cdi.internal.virtualization.deckhouse.io: "true" -{{- end }} diff --git a/templates/kubevirt/virt-operator/rbac-for-us.yaml b/templates/kubevirt/virt-operator/rbac-for-us.yaml index c3c7b0119a..10db7e8699 100644 --- a/templates/kubevirt/virt-operator/rbac-for-us.yaml +++ b/templates/kubevirt/virt-operator/rbac-for-us.yaml @@ -378,15 +378,6 @@ rules: - get - list - watch -- apiGroups: - - cdi.internal.virtualization.deckhouse.io - resources: - - internalvirtualizationdatasources - - internalvirtualizationdatavolumes - verbs: - - get - - list - - watch - apiGroups: - instancetype.internal.virtualization.deckhouse.io resources: @@ -607,12 +598,6 @@ rules: - virtualmachineinstances/softreboot verbs: - update -- apiGroups: - - cdi.internal.virtualization.deckhouse.io - resources: - - '*' - verbs: - - '*' - apiGroups: - k8s.cni.cncf.io resources: diff --git a/templates/pre-delete-hook/job.yaml b/templates/pre-delete-hook/job.yaml index 15af983434..773614ad53 100644 --- a/templates/pre-delete-hook/job.yaml +++ b/templates/pre-delete-hook/job.yaml @@ -34,14 +34,6 @@ spec: }, "namespace": "d8-{{ .Chart.Name }}", "name": "config" - }, - { - "gvr": { - "Group": "cdi.internal.virtualization.deckhouse.io", - "Version": "v1beta1", - "Resource": "internalvirtualizationcdis" - }, - "name": "config" } ] resources: diff --git a/templates/pre-delete-hook/rbac-for-us.yaml b/templates/pre-delete-hook/rbac-for-us.yaml index 12f78d785e..56b40a25e7 100644 --- a/templates/pre-delete-hook/rbac-for-us.yaml +++ b/templates/pre-delete-hook/rbac-for-us.yaml @@ -21,13 +21,6 @@ rules: verbs: - get - delete -- apiGroups: - - cdi.internal.virtualization.deckhouse.io - resources: - - internalvirtualizationcdis - verbs: - - get - - delete --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/templates/rbac-to-us.yaml b/templates/rbac-to-us.yaml index 057284d2d7..15fffc9fd8 100644 --- a/templates/rbac-to-us.yaml +++ b/templates/rbac-to-us.yaml @@ -8,7 +8,7 @@ metadata: rules: - apiGroups: ["apps"] resources: ["daemonsets/prometheus-metrics", "deployments/prometheus-metrics"] - resourceNames: ["dvcr", "virtualization-controller", "kube-api-rewriter", "virt-handler", "virt-api", "virt-controller", "cdi-deployment", "cdi-operator"] + resourceNames: ["dvcr", "virtualization-controller", "kube-api-rewriter", "virt-handler", "virt-api", "virt-controller"] verbs: ["get"] {{- if (.Values.global.enabledModules | has "prometheus") }} diff --git a/templates/rbacv2/manage/permissions/manage_internals.yaml b/templates/rbacv2/manage/permissions/manage_internals.yaml index 3498faa55c..406699d0c3 100644 --- a/templates/rbacv2/manage/permissions/manage_internals.yaml +++ b/templates/rbacv2/manage/permissions/manage_internals.yaml @@ -13,8 +13,6 @@ rules: - apiGroups: - cdi.internal.virtualization.deckhouse.io resources: - - internalvirtualizationcdiconfigs - - internalvirtualizationcdis - internalvirtualizationstorageprofiles verbs: - create diff --git a/templates/rbacv2/use/capabilities/manage_internals.yaml b/templates/rbacv2/use/capabilities/manage_internals.yaml index 6e45c53b35..a700181f4a 100644 --- a/templates/rbacv2/use/capabilities/manage_internals.yaml +++ b/templates/rbacv2/use/capabilities/manage_internals.yaml @@ -8,14 +8,6 @@ metadata: rbac.deckhouse.io/kind: use name: d8:use:capability:virtualization:manage_internals rules: - - apiGroups: - - cdi.internal.virtualization.deckhouse.io - resources: - - internalvirtualizationdatavolumes - verbs: - - get - - list - - watch - apiGroups: - internal.virtualization.deckhouse.io resources: diff --git a/templates/virtualization-controller/_helpers.tpl b/templates/virtualization-controller/_helpers.tpl index 3d799bf0fd..0b86529853 100644 --- a/templates/virtualization-controller/_helpers.tpl +++ b/templates/virtualization-controller/_helpers.tpl @@ -24,6 +24,8 @@ true fieldPath: metadata.namespace - name: IMPORTER_IMAGE value: {{ include "helm_lib_module_image" (list . "dvcrImporter") }} +- name: DISK_IMPORTER_IMAGE + value: {{ include "helm_lib_module_image" (list . "virtualDiskImporter") }} - name: UPLOADER_IMAGE value: {{ include "helm_lib_module_image" (list . "dvcrUploader") }} - name: BOUNDER_IMAGE diff --git a/templates/virtualization-controller/rbac-for-us.yaml b/templates/virtualization-controller/rbac-for-us.yaml index 8bc92dc654..673a0124db 100644 --- a/templates/virtualization-controller/rbac-for-us.yaml +++ b/templates/virtualization-controller/rbac-for-us.yaml @@ -91,6 +91,7 @@ rules: - storage.k8s.io resources: - storageclasses + - csidrivers verbs: - get - list @@ -120,7 +121,7 @@ rules: - apiGroups: - cdi.internal.virtualization.deckhouse.io resources: - - internalvirtualizationdatavolumes + - internalvirtualizationstorageprofiles verbs: - get - create @@ -144,17 +145,18 @@ rules: - list - delete - apiGroups: - - snapshot.storage.k8s.io + - snapshot.storage.k8s.io resources: - - volumesnapshots + - volumesnapshots + - volumesnapshotclasses verbs: - - get - - watch - - create - - patch - - update - - list - - delete + - get + - watch + - create + - patch + - update + - list + - delete - apiGroups: - internal.virtualization.deckhouse.io resources: diff --git a/test/e2e/blockdevice/virtual_disk_creation.go b/test/e2e/blockdevice/virtual_disk_creation.go new file mode 100644 index 0000000000..48ed23bf5d --- /dev/null +++ b/test/e2e/blockdevice/virtual_disk_creation.go @@ -0,0 +1,503 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package blockdevice + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + netv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" + vdsnapshotbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vdsnapshot" + vibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vi" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/object" + vdobs "github.com/deckhouse/virtualization/test/e2e/internal/observer/vd" + viobs "github.com/deckhouse/virtualization/test/e2e/internal/observer/vi" + "github.com/deckhouse/virtualization/test/e2e/internal/precheck" + "github.com/deckhouse/virtualization/test/e2e/internal/util" +) + +// vdCreationStorageClass is the storage class used by VirtualDiskCreation tests until +// the e2e environment exposes this as a configurable parameter. +const vdCreationStorageClass = "linstor-thin-r1-immediate" + +const vdCreationBlankSize = "64Mi" + +var _ = Describe("VirtualDiskCreation", Ordered, Label(precheck.NoPrecheck), func() { + var ( + f *framework.Framework + ctx context.Context + + scPtr *string + ) + + BeforeAll(func() { + ctx = context.Background() + f = framework.NewFramework("vd-creation") + f.Before() + DeferCleanup(f.After) + + scPtr = ptr.To(vdCreationStorageClass) + }) + + It("provisions a VirtualDisk from HTTP data source", Label(precheck.NoPrecheck), func() { + vd := vdbuilder.New( + vdbuilder.WithName("vd-http"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{URL: object.ImageTestDataQCOW}), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + + It("provisions a VirtualDisk from Upload data source", Label(precheck.NoPrecheck), func() { + vd := vdbuilder.New( + vdbuilder.WithName("vd-upload"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDatasource(&v1alpha2.VirtualDiskDataSource{ + Type: v1alpha2.DataSourceTypeUpload, + }), + vdbuilder.WithStorageClass(scPtr), + ) + + var uploadFilePath string + By("Downloading source image to upload", func() { + var err error + uploadFilePath, err = downloadImageToTempFile(object.ImageTestDataQCOW) + Expect(err).NotTo(HaveOccurred(), "failed to download upload source image") + DeferCleanup(func() { + removeErr := os.Remove(uploadFilePath) + Expect(removeErr == nil || errors.Is(removeErr, os.ErrNotExist)).To(BeTrue(), + "failed to remove upload source file %q: %v", uploadFilePath, removeErr) + }) + }) + + obs := vdobs.StartObserver(ctx, f, vd) + obs.Never(vdobs.BeFailed()) + obs.Always(vdobs.BeStorageClassReady()) + obs.Always(vdobs.BeDataSourceReady()) + obs.Always(vdobs.HaveNonDecreasingProgress()) + + By("Creating VirtualDisk", func() { + err := f.CreateWithDeferredDeletion(ctx, vd) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Waiting for the VirtualDisk to expose upload URLs", func() { + err := obs.WaitFor(vdobs.BeReadyForUserUpload(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Allowing ingress-nginx to reach the uploader pod (workaround)", func() { + err := allowIngressNginxToUploaderNetworkPolicy(ctx, f, vd.Namespace, vd.UID) + Expect(err).NotTo(HaveOccurred(), "failed to patch uploader NetworkPolicy") + }) + + By("Uploading data to the VirtualDisk", func() { + err := f.Clients.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vd), vd) + Expect(err).NotTo(HaveOccurred()) + Expect(vd.Status.ImageUploadURLs).NotTo(BeNil()) + Expect(vd.Status.ImageUploadURLs.External).NotTo(BeEmpty()) + + err = doRetriableUploadAttempt(vd.Status.ImageUploadURLs.External, uploadFilePath) + Expect(err).NotTo(HaveOccurred(), "upload should succeed") + }) + + err := obs.WaitFor(vdobs.BeReady(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) + }) + + It("provisions a VirtualDisk from ContainerImage (registry) data source", Label(precheck.NoPrecheck), func() { + vd := vdbuilder.New( + vdbuilder.WithName("vd-registry"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceContainerImage(object.ImageURLContainerImage, "", nil), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + + It("provisions a VirtualDisk from a VirtualImage on DVCR", Label(precheck.NoPrecheck), func() { + baseVI := vibuilder.New( + vibuilder.WithName("vi-source-dvcr"), + vibuilder.WithNamespace(f.Namespace().Name), + vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), + vibuilder.WithDataSourceHTTP(object.ImageTestDataQCOW, nil, nil), + ) + + viObs := viobs.StartObserver(ctx, f, baseVI) + viObs.Never(viobs.BeFailed()) + + By("Creating base VirtualImage on DVCR", func() { + err := f.CreateWithDeferredDeletion(ctx, baseVI) + Expect(err).NotTo(HaveOccurred()) + + err = viObs.WaitFor(viobs.BeReady(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) + }) + + vd := vdbuilder.New( + vdbuilder.WithName("vd-from-vi"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceObjectRef(v1alpha2.VirtualDiskObjectRefKindVirtualImage, baseVI.Name), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + + It("provisions a VirtualDisk from a VirtualImage on PVC", Label(precheck.NoPrecheck), func() { + baseVI := vibuilder.New( + vibuilder.WithName("vi-source-pvc"), + vibuilder.WithNamespace(f.Namespace().Name), + vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim), + vibuilder.WithDataSourceHTTP(object.ImageTestDataQCOW, nil, nil), + ) + baseVI.Spec.PersistentVolumeClaim.StorageClass = scPtr + + viObs := viobs.StartObserver(ctx, f, baseVI) + viObs.Never(viobs.BeFailed()) + + By("Creating base VirtualImage on PVC", func() { + err := f.CreateWithDeferredDeletion(ctx, baseVI) + Expect(err).NotTo(HaveOccurred()) + + err = viObs.WaitFor(viobs.BeReady(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) + }) + + vd := vdbuilder.New( + vdbuilder.WithName("vd-from-vi-pvc"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceObjectRef(v1alpha2.VirtualDiskObjectRefKindVirtualImage, baseVI.Name), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + + It("provisions a VirtualDisk from a ClusterVirtualImage", Label(precheck.NoPrecheck), func() { + vd := vdbuilder.New( + vdbuilder.WithName("vd-from-cvi"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceObjectRef(v1alpha2.VirtualDiskObjectRefKindClusterVirtualImage, object.PrecreatedCVITestDataQCOW), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + + It("provisions a blank VirtualDisk", Label(precheck.NoPrecheck), func() { + vd := vdbuilder.New( + vdbuilder.WithName("vd-blank"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithPersistentVolumeClaim(scPtr, ptr.To(resource.MustParse(vdCreationBlankSize))), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + + Context("with snapshots", Label(precheck.PrecheckSnapshot), func() { + It("provisions a VirtualDisk from a VirtualDiskSnapshot", func() { + baseVD := vdbuilder.New( + vdbuilder.WithName("vd-source-for-snapshot"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{URL: object.ImageTestDataQCOW}), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, baseVD) + + vdSnapshot := vdsnapshotbuilder.New( + vdsnapshotbuilder.WithName("vd-snapshot"), + vdsnapshotbuilder.WithNamespace(f.Namespace().Name), + vdsnapshotbuilder.WithVirtualDiskName(baseVD.Name), + vdsnapshotbuilder.WithRequiredConsistency(true), + ) + + By("Creating VirtualDiskSnapshot", func() { + err := f.CreateWithDeferredDeletion(ctx, vdSnapshot) + Expect(err).NotTo(HaveOccurred()) + + util.UntilObjectPhase(ctx, string(v1alpha2.VirtualDiskSnapshotPhaseReady), framework.LongTimeout, vdSnapshot) + }) + + vd := vdbuilder.New( + vdbuilder.WithName("vd-from-snapshot"), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceObjectRef(v1alpha2.VirtualDiskObjectRefKindVirtualDiskSnapshot, vdSnapshot.Name), + vdbuilder.WithStorageClass(scPtr), + ) + + createVirtualDiskAndWait(ctx, f, vd) + }) + }) +}) + +func createVirtualDiskAndWait(ctx context.Context, f *framework.Framework, vd *v1alpha2.VirtualDisk) { + GinkgoHelper() + + obs := vdobs.StartObserver(ctx, f, vd) + obs.Never(vdobs.BeFailed()) + obs.Always(vdobs.BeStorageClassReady()) + obs.Always(vdobs.BeDataSourceReady()) + obs.Always(vdobs.HaveNonDecreasingProgress()) + + By("Creating VirtualDisk", func() { + err := f.CreateWithDeferredDeletion(ctx, vd) + Expect(err).NotTo(HaveOccurred()) + }) + + err := obs.WaitFor(vdobs.BeReady(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) +} + +func doRetriableUploadAttempt(url, filePath string) error { + const maxAttempts = 12 + const retryDelay = 5 * time.Second + + var lastErr error + for attempt := 1; attempt <= maxAttempts; attempt++ { + err := doVirtualDiskUploadAttempt(url, filePath) + if err == nil { + return nil + } + if !isRetriableUploadError(err) { + return err + } + + lastErr = err + time.Sleep(retryDelay) + } + + return fmt.Errorf("upload failed after %d attempts: %w", maxAttempts, lastErr) +} + +func doVirtualDiskUploadAttempt(url, filePath string) error { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open %s: %w", filePath, err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil && !errors.Is(closeErr, os.ErrClosed) { + Expect(closeErr).NotTo(HaveOccurred(), "Failed to close file %s", filePath) + } + }() + + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to stat %s: %w", filePath, err) + } + if stat.Size() == 0 { + return fmt.Errorf("file %s is empty", filePath) + } + + req, err := http.NewRequest(http.MethodPut, url, file) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %w", err) + } + req.ContentLength = stat.Size() + + resp, err := client.Do(req) + if err != nil { + return err + } + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil && !errors.Is(closeErr, os.ErrClosed) { + Expect(closeErr).NotTo(HaveOccurred(), "Failed to close response body") + } + }() + + return handleUploadResponse(resp) +} + +func isRetriableUploadError(err error) bool { + message := err.Error() + return !strings.Contains(message, "upload failed with status ") || + strings.Contains(message, "upload failed with status 5") +} + +// downloadImageToTempFile downloads url into a temporary file and returns its path. +// The caller is responsible for removing the file when finished. +func downloadImageToTempFile(url string) (string, error) { + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + resp, err := httpClient.Get(url) + if err != nil { + return "", fmt.Errorf("download %q: %w", url, err) + } + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil && !errors.Is(closeErr, os.ErrClosed) { + Expect(closeErr).NotTo(HaveOccurred(), "failed to close response body") + } + }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("download %q: unexpected status %d", url, resp.StatusCode) + } + + tmpFile, err := os.CreateTemp("", filepath.Base(url)+"-*") + if err != nil { + return "", fmt.Errorf("create temp file: %w", err) + } + closed := false + defer func() { + if closed { + return + } + if closeErr := tmpFile.Close(); closeErr != nil && !errors.Is(closeErr, os.ErrClosed) { + Expect(closeErr).NotTo(HaveOccurred(), "failed to close temp file") + } + }() + + if _, err := io.Copy(tmpFile, resp.Body); err != nil { + return "", fmt.Errorf("copy to temp file: %w", err) + } + + if err := tmpFile.Close(); err != nil { + return "", fmt.Errorf("close temp file: %w", err) + } + closed = true + + return tmpFile.Name(), nil +} + +// uploaderIngressNginxNamespaceLabel is the namespace label used to match the +// Deckhouse ingress-nginx controller namespace. +const uploaderIngressNginxNamespaceLabel = "module" + +// uploaderIngressNginxNamespaceLabelValue is the value of the namespace label +// for the Deckhouse ingress-nginx controller namespace (d8-ingress-nginx). +const uploaderIngressNginxNamespaceLabelValue = "ingress-nginx" + +// allowIngressNginxToUploaderNetworkPolicy patches the NetworkPolicy created by +// the virtualization-controller for the uploader pod owned by vd, so that +// traffic from the Deckhouse ingress-nginx controller namespace +// (d8-ingress-nginx) is allowed to reach the uploader pod. +// +// Without this patch external uploads via the Ingress URL fail with a 504 +// Gateway Time-out because the NetworkPolicy currently only allows ingress +// from namespaces with the label "module=virtualization", while the ingress +// controller pod lives in "d8-ingress-nginx" (label "module=ingress-nginx"). +func allowIngressNginxToUploaderNetworkPolicy(ctx context.Context, f *framework.Framework, namespace string, ownerUID types.UID) error { + var policies netv1.NetworkPolicyList + if err := f.Clients.GenericClient().List(ctx, &policies, crclient.InNamespace(namespace)); err != nil { + return fmt.Errorf("list network policies in %q: %w", namespace, err) + } + + peer := netv1.NetworkPolicyPeer{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + uploaderIngressNginxNamespaceLabel: uploaderIngressNginxNamespaceLabelValue, + }, + }, + } + + var patched int + for i := range policies.Items { + np := &policies.Items[i] + if !isOwnedByUID(np.OwnerReferences, ownerUID) { + continue + } + if hasNamespaceSelectorPeer(np.Spec.Ingress, peer.NamespaceSelector.MatchLabels) { + patched++ + continue + } + + if len(np.Spec.Ingress) == 0 { + np.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{}} + } + np.Spec.Ingress[0].From = append(np.Spec.Ingress[0].From, peer) + + if err := f.Clients.GenericClient().Update(ctx, np); err != nil { + return fmt.Errorf("update network policy %q: %w", np.Name, err) + } + patched++ + } + + if patched == 0 { + return fmt.Errorf("no NetworkPolicy owned by UID %q found in %q", ownerUID, namespace) + } + return nil +} + +func isOwnedByUID(refs []metav1.OwnerReference, uid types.UID) bool { + for _, ref := range refs { + if ref.UID == uid { + return true + } + } + return false +} + +func hasNamespaceSelectorPeer(rules []netv1.NetworkPolicyIngressRule, labels map[string]string) bool { + for _, rule := range rules { + for _, from := range rule.From { + if from.NamespaceSelector == nil { + continue + } + if equalLabels(from.NamespaceSelector.MatchLabels, labels) { + return true + } + } + } + return false +} + +func equalLabels(a, b map[string]string) bool { + if len(a) != len(b) { + return false + } + for k, v := range a { + if b[k] != v { + return false + } + } + return true +} diff --git a/test/e2e/blockdevice/virtual_image_creation.go b/test/e2e/blockdevice/virtual_image_creation.go index e0bc120762..570d090a4b 100644 --- a/test/e2e/blockdevice/virtual_image_creation.go +++ b/test/e2e/blockdevice/virtual_image_creation.go @@ -1,11 +1,11 @@ /* -Copyright 2025 Flant JSC +Copyright 2026 Flant JSC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,256 +18,286 @@ package blockdevice import ( "context" - "fmt" + "errors" + "os" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" + crclient "sigs.k8s.io/controller-runtime/pkg/client" - cvibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/cvi" vdbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vd" vdsnapshotbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vdsnapshot" vibuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vi" - vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/test/e2e/internal/framework" "github.com/deckhouse/virtualization/test/e2e/internal/object" + viobs "github.com/deckhouse/virtualization/test/e2e/internal/observer/vi" "github.com/deckhouse/virtualization/test/e2e/internal/precheck" "github.com/deckhouse/virtualization/test/e2e/internal/util" ) -var _ = Describe("VirtualImageCreation", Label(precheck.PrecheckSnapshot), func() { +var _ = Describe("VirtualImageCreation", Ordered, Label(precheck.NoPrecheck), func() { var ( f *framework.Framework ctx context.Context + + scPtr *string ) - BeforeEach(func() { + BeforeAll(func() { ctx = context.Background() f = framework.NewFramework("vi-creation") - sc := framework.GetConfig().StorageClass.TemplateStorageClass - if sc != nil && sc.Provisioner == framework.NFS { - Skip("VirtualImages on PVC only work with block storage classes, skipping NFS") - } - f.Before() DeferCleanup(f.After) + + scPtr = ptr.To(vdCreationStorageClass) }) - It("verifies the images are created successfully", func() { - const cviPrefix = "v12-e2e" - var ( - vd *v1alpha2.VirtualDisk - vdSnapshot *v1alpha2.VirtualDiskSnapshot - vis []*v1alpha2.VirtualImage - cvis []*v1alpha2.ClusterVirtualImage + It("provisions VirtualImages on DVCR and PVC from HTTP data source", Label(precheck.NoPrecheck), func() { + viDVCR := newVirtualImageOnDVCR("vi-http", + vibuilder.WithDataSourceHTTP(object.ImageTestDataQCOW, nil, nil), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-http", scPtr, + vibuilder.WithDataSourceHTTP(object.ImageTestDataQCOW, nil, nil), + ) + + createVirtualImageAndWait(ctx, f, viDVCR) - baseCvis []*v1alpha2.ClusterVirtualImage - baseVis []*v1alpha2.VirtualImage + createVirtualImageAndWait(ctx, f, viPVC) + }) + + It("provisions VirtualImages on DVCR and PVC from Upload data source", Label(precheck.NoPrecheck), func() { + viDVCR := newVirtualImageOnDVCR("vi-upload", + vibuilder.WithDatasource(v1alpha2.VirtualImageDataSource{ + Type: v1alpha2.DataSourceTypeUpload, + }), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-upload", scPtr, + vibuilder.WithDatasource(v1alpha2.VirtualImageDataSource{ + Type: v1alpha2.DataSourceTypeUpload, + }), ) - By("Creating VirtualDisk", func() { - vd = vdbuilder.New( - vdbuilder.WithGenerateName("vd-"), - vdbuilder.WithNamespace(f.Namespace().Name), - vdbuilder.WithSize(ptr.To(resource.MustParse("350Mi"))), - vdbuilder.WithDataSourceHTTP( - &v1alpha2.DataSourceHTTP{ - URL: object.ImageURLAlpineUEFI, - }, - ), - ) - err := f.CreateWithDeferredDeletion(ctx, vd) - Expect(err).NotTo(HaveOccurred()) - vm := object.NewMinimalVM("vm-", f.Namespace().Name, vmbuilder.WithBlockDeviceRefs(v1alpha2.BlockDeviceSpecRef{ - Kind: v1alpha2.VirtualDiskKind, - Name: vd.Name, - })) - err = f.CreateWithDeferredDeletion(ctx, vm) - Expect(err).NotTo(HaveOccurred()) - util.UntilObjectPhase(ctx, string(v1alpha2.DiskReady), framework.LongTimeout, vd) - err = f.Delete(ctx, vm) - Expect(err).NotTo(HaveOccurred()) + var uploadFilePath string + By("Downloading source image to upload", func() { + var err error + uploadFilePath, err = downloadImageToTempFile(object.ImageTestDataQCOW) + Expect(err).NotTo(HaveOccurred(), "failed to download upload source image") + DeferCleanup(func() { + removeErr := os.Remove(uploadFilePath) + Expect(removeErr == nil || errors.Is(removeErr, os.ErrNotExist)).To(BeTrue(), + "failed to remove upload source file %q: %v", uploadFilePath, removeErr) + }) }) - By("Creating VirtualDiskSnapshot", func() { - vdSnapshot = vdsnapshotbuilder.New( - vdsnapshotbuilder.WithGenerateName("vdsnapshot-"), + uploadVirtualImageAndWait(ctx, f, viDVCR, uploadFilePath) + + uploadVirtualImageAndWait(ctx, f, viPVC, uploadFilePath) + }) + + It("provisions VirtualImages on DVCR and PVC from ContainerImage (registry) data source", Label(precheck.NoPrecheck), func() { + viDVCR := newVirtualImageOnDVCR("vi-registry", + vibuilder.WithDataSourceContainerImage(object.ImageURLContainerImage, v1alpha2.ImagePullSecretName{}, nil), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-registry", scPtr, + vibuilder.WithDataSourceContainerImage(object.ImageURLContainerImage, v1alpha2.ImagePullSecretName{}, nil), + ) + + createVirtualImageAndWait(ctx, f, viDVCR) + + createVirtualImageAndWait(ctx, f, viPVC) + }) + + It("provisions VirtualImages on DVCR and PVC from a VirtualDisk", Label(precheck.NoPrecheck), func() { + vd := createHTTPVirtualDiskAndWait(ctx, f, "vd-source-for-vi", scPtr) + + viDVCR := newVirtualImageOnDVCR("vi-from-vd", + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDisk, vd.Name), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-from-vd", scPtr, + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDisk, vd.Name), + ) + + createVirtualImageAndWait(ctx, f, viDVCR) + + createVirtualImageAndWait(ctx, f, viPVC) + }) + + It("provisions VirtualImages on DVCR and PVC from a ClusterVirtualImage", Label(precheck.NoPrecheck), func() { + viDVCR := newVirtualImageOnDVCR("vi-from-cvi", + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindClusterVirtualImage, object.PrecreatedCVITestDataQCOW), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-from-cvi", scPtr, + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindClusterVirtualImage, object.PrecreatedCVITestDataQCOW), + ) + + createVirtualImageAndWait(ctx, f, viDVCR) + + createVirtualImageAndWait(ctx, f, viPVC) + }) + + It("provisions VirtualImages on DVCR and PVC from a VirtualImage on DVCR", Label(precheck.NoPrecheck), func() { + baseVI := newVirtualImageOnDVCR("vi-source-dvcr", + vibuilder.WithDataSourceHTTP(object.ImageTestDataQCOW, nil, nil), + ) + createVirtualImageAndWait(ctx, f, baseVI) + + viDVCR := newVirtualImageOnDVCR("vi-from-vi-dvcr", + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualImage, baseVI.Name), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-from-vi-dvcr", scPtr, + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualImage, baseVI.Name), + ) + + createVirtualImageAndWait(ctx, f, viDVCR) + + createVirtualImageAndWait(ctx, f, viPVC) + }) + + It("provisions VirtualImages on DVCR and PVC from a VirtualImage on PVC", Label(precheck.NoPrecheck), func() { + baseVI := newVirtualImageOnPVC("vi-source-pvc", scPtr, + vibuilder.WithDataSourceHTTP(object.ImageTestDataQCOW, nil, nil), + ) + createVirtualImageAndWait(ctx, f, baseVI) + + viDVCR := newVirtualImageOnDVCR("vi-from-vi-pvc", + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualImage, baseVI.Name), + ) + viPVC := newVirtualImageOnPVC("vi-pvc-from-vi-pvc", scPtr, + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualImage, baseVI.Name), + ) + + createVirtualImageAndWait(ctx, f, viDVCR) + + createVirtualImageAndWait(ctx, f, viPVC) + }) + + Context("with snapshots", Label(precheck.PrecheckSnapshot), func() { + It("provisions VirtualImages on DVCR and PVC from a VirtualDiskSnapshot", func() { + vd := createHTTPVirtualDiskAndWait(ctx, f, "vd-source-for-vi-snapshot", scPtr) + + vdSnapshot := vdsnapshotbuilder.New( + vdsnapshotbuilder.WithName("vdsnapshot-source-for-vi"), vdsnapshotbuilder.WithNamespace(f.Namespace().Name), vdsnapshotbuilder.WithVirtualDiskName(vd.Name), vdsnapshotbuilder.WithRequiredConsistency(true), ) - err := f.CreateWithDeferredDeletion(ctx, vdSnapshot) - Expect(err).NotTo(HaveOccurred()) - util.UntilObjectPhase(ctx, string(v1alpha2.VirtualDiskSnapshotPhaseReady), framework.ShortTimeout, vdSnapshot) - }) - By("Generating base cvis", func() { - baseCvis = append(baseCvis, object.NewGenerateContainerImageCVI(fmt.Sprintf("%s-cvi-ci-", cviPrefix))) - baseCvis = append(baseCvis, cvibuilder.New( - cvibuilder.WithGenerateName(fmt.Sprintf("%s-cvi-http-", cviPrefix)), - cvibuilder.WithDataSourceHTTP( - object.ImageURLAlpineUEFI, nil, nil, - ), - )) - baseCvis = append(baseCvis, cvibuilder.New( - cvibuilder.WithGenerateName(fmt.Sprintf("%s-cvi-from-vd-", cviPrefix)), - cvibuilder.WithDataSourceObjectRef(v1alpha2.ClusterVirtualImageObjectRefKindVirtualDisk, vd.Name, f.Namespace().Name), - )) - baseCvis = append(baseCvis, cvibuilder.New( - cvibuilder.WithGenerateName(fmt.Sprintf("%s-cvi-from-vds-", cviPrefix)), - cvibuilder.WithDataSourceObjectRef(v1alpha2.ClusterVirtualImageObjectRefKindVirtualDiskSnapshot, vdSnapshot.Name, f.Namespace().Name), - )) - }) + By("Creating VirtualDiskSnapshot", func() { + err := f.CreateWithDeferredDeletion(ctx, vdSnapshot) + Expect(err).NotTo(HaveOccurred()) - By("Generating base vis on dvcr", func() { - baseVis = append(baseVis, object.NewGeneratedContainerImageVI("vi-ci-", f.Namespace().Name)) - baseVis = append(baseVis, vibuilder.New( - vibuilder.WithGenerateName("vi-http-"), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), - vibuilder.WithDataSourceHTTP( - object.ImageURLAlpineUEFI, nil, nil, - ), - )) - baseVis = append(baseVis, vibuilder.New( - vibuilder.WithGenerateName("vi-from-vd-"), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDisk, vd.Name), - )) - baseVis = append(baseVis, vibuilder.New( - vibuilder.WithGenerateName("vi-from-vds-"), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDiskSnapshot, vdSnapshot.Name), - )) - }) + util.UntilObjectPhase(ctx, string(v1alpha2.VirtualDiskSnapshotPhaseReady), framework.LongTimeout, vdSnapshot) + }) - By("Generating base vis on pvc", func() { - baseVis = append(baseVis, object.NewGeneratedContainerImageVI("vi-pvc-ci-", f.Namespace().Name, vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim))) - baseVis = append(baseVis, vibuilder.New( - vibuilder.WithGenerateName("vi-http-"), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim), - vibuilder.WithDataSourceHTTP( - object.ImageURLAlpineUEFI, nil, nil, - ), - )) - baseVis = append(baseVis, vibuilder.New( - vibuilder.WithGenerateName("vi-pvc-from-vd-"), - vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDisk, vd.Name), - )) - baseVis = append(baseVis, vibuilder.New( - vibuilder.WithGenerateName("vi-pvc-from-vds-"), - vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim), - vibuilder.WithNamespace(f.Namespace().Name), + viDVCR := newVirtualImageOnDVCR("vi-from-vdsnapshot", vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDiskSnapshot, vdSnapshot.Name), - )) - }) + ) + viPVC := newVirtualImageOnPVC("vi-pvc-from-vdsnapshot", scPtr, + vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualDiskSnapshot, vdSnapshot.Name), + ) - By("Creating base images", func() { - for _, cvi := range baseCvis { - err := f.CreateWithDeferredDeletion(ctx, cvi) - Expect(err).NotTo(HaveOccurred()) - } - for _, vi := range baseVis { - err := f.CreateWithDeferredDeletion(ctx, vi) - Expect(err).NotTo(HaveOccurred()) - } - }) + createVirtualImageAndWait(ctx, f, viDVCR) - By("Generating cvis from base cvis", func() { - for _, baseCvi := range baseCvis { - cvis = append(cvis, cvibuilder.New( - cvibuilder.WithName(fmt.Sprintf("%s-cvi-from-%s", cviPrefix, baseCvi.Name)), - cvibuilder.WithDataSourceObjectRef(v1alpha2.ClusterVirtualImageObjectRefKindClusterVirtualImage, baseCvi.Name, ""), - )) - } + createVirtualImageAndWait(ctx, f, viPVC) }) + }) +}) - By("Generating cvis from base vis", func() { - for _, baseVi := range baseVis { - cvis = append(cvis, cvibuilder.New( - cvibuilder.WithName(fmt.Sprintf("%s-cvi-from-%s", cviPrefix, baseVi.Name)), - cvibuilder.WithDataSourceObjectRef(v1alpha2.ClusterVirtualImageObjectRefKindVirtualImage, baseVi.Name, baseVi.Namespace), - )) - } - }) +func newVirtualImageOnDVCR(name string, opts ...vibuilder.Option) *v1alpha2.VirtualImage { + baseOpts := []vibuilder.Option{ + vibuilder.WithName(name), + vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), + } + baseOpts = append(baseOpts, opts...) + return vibuilder.New(baseOpts...) +} - By("Generating dvcr vis from base cvis", func() { - for _, baseCvi := range baseCvis { - vis = append(vis, vibuilder.New( - vibuilder.WithName(fmt.Sprintf("vi-from-%s", baseCvi.Name)), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindClusterVirtualImage, baseCvi.Name), - vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), - )) - } - }) +func newVirtualImageOnPVC(name string, sc *string, opts ...vibuilder.Option) *v1alpha2.VirtualImage { + vi := newVirtualImageOnDVCR(name, + append([]vibuilder.Option{vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim)}, opts...)..., + ) + vi.Spec.PersistentVolumeClaim.StorageClass = sc + return vi +} - By("Generating dvcr vis from base vis", func() { - for _, baseVi := range baseVis { - vis = append(vis, vibuilder.New( - vibuilder.WithName(fmt.Sprintf("vi-from-%s", baseVi.Name)), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithStorage(v1alpha2.StorageContainerRegistry), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualImage, baseVi.Name), - )) - } - }) +func createVirtualImageAndWait(ctx context.Context, f *framework.Framework, vi *v1alpha2.VirtualImage) { + GinkgoHelper() - By("Generating pvc vis from base cvis", func() { - for _, baseCvi := range baseCvis { - vis = append(vis, vibuilder.New( - vibuilder.WithName(fmt.Sprintf("vi-pvc-from-%s", baseCvi.Name)), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindClusterVirtualImage, baseCvi.Name), - vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim), - )) - } - }) + vi.Namespace = f.Namespace().Name + obs := viobs.StartObserver(ctx, f, vi) + obs.Never(viobs.BeFailed()) + obs.Always(viobs.HaveNonDecreasingProgress()) - By("Generating pvc vis from base vis", func() { - for _, baseVi := range baseVis { - vis = append(vis, vibuilder.New( - vibuilder.WithName(fmt.Sprintf("vi-pvc-from-%s", baseVi.Name)), - vibuilder.WithNamespace(f.Namespace().Name), - vibuilder.WithStorage(v1alpha2.StoragePersistentVolumeClaim), - vibuilder.WithDataSourceObjectRef(v1alpha2.VirtualImageObjectRefKindVirtualImage, baseVi.Name), - )) - } - }) + By("Creating VirtualImage on "+virtualImageStorageName(vi), func() { + err := f.CreateWithDeferredDeletion(ctx, vi) + Expect(err).NotTo(HaveOccurred()) + }) - By("Creating images", func() { - for _, vi := range vis { - err := f.CreateWithDeferredDeletion(ctx, vi) - Expect(err).NotTo(HaveOccurred()) - } + err := obs.WaitFor(viobs.BeReady(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) +} - for _, cvi := range cvis { - err := f.CreateWithDeferredDeletion(ctx, cvi) - Expect(err).NotTo(HaveOccurred()) - } - }) +func uploadVirtualImageAndWait(ctx context.Context, f *framework.Framework, vi *v1alpha2.VirtualImage, uploadFilePath string) { + GinkgoHelper() - By("Verifying that images are ready", func() { - // Should check base images too - vis = append(baseVis, vis...) - cvis = append(baseCvis, cvis...) - - var objects []client.Object - for _, vi := range vis { - objects = append(objects, vi) - } - for _, cvi := range cvis { - objects = append(objects, cvi) - } - util.UntilObjectPhase(ctx, string(v1alpha2.ImageReady), framework.LongTimeout, objects...) - }) + vi.Namespace = f.Namespace().Name + obs := viobs.StartObserver(ctx, f, vi) + obs.Never(viobs.BeFailed()) + obs.Always(viobs.HaveNonDecreasingProgress()) + + By("Creating VirtualImage on "+virtualImageStorageName(vi), func() { + err := f.CreateWithDeferredDeletion(ctx, vi) + Expect(err).NotTo(HaveOccurred()) }) -}) + + By("Waiting for the VirtualImage to expose upload URLs", func() { + err := obs.WaitFor(viobs.BeReadyForUserUpload(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Allowing ingress-nginx to reach the uploader pod (workaround)", func() { + err := allowIngressNginxToUploaderNetworkPolicy(ctx, f, vi.Namespace, vi.UID) + Expect(err).NotTo(HaveOccurred(), "failed to patch uploader NetworkPolicy") + }) + + By("Uploading data to the VirtualImage", func() { + err := f.Clients.GenericClient().Get(ctx, crclient.ObjectKeyFromObject(vi), vi) + Expect(err).NotTo(HaveOccurred()) + Expect(vi.Status.ImageUploadURLs).NotTo(BeNil()) + Expect(vi.Status.ImageUploadURLs.External).NotTo(BeEmpty()) + + err = doRetriableUploadAttempt(vi.Status.ImageUploadURLs.External, uploadFilePath) + Expect(err).NotTo(HaveOccurred(), "upload should succeed") + }) + + err := obs.WaitFor(viobs.BeReady(), framework.LongTimeout) + Expect(err).NotTo(HaveOccurred()) +} + +func createHTTPVirtualDiskAndWait(ctx context.Context, f *framework.Framework, name string, sc *string) *v1alpha2.VirtualDisk { + GinkgoHelper() + + vd := vdbuilder.New( + vdbuilder.WithName(name), + vdbuilder.WithNamespace(f.Namespace().Name), + vdbuilder.WithDataSourceHTTP(&v1alpha2.DataSourceHTTP{URL: object.ImageTestDataQCOW}), + vdbuilder.WithStorageClass(sc), + ) + + createVirtualDiskAndWait(ctx, f, vd) + + return vd +} + +func virtualImageStorageName(vi *v1alpha2.VirtualImage) string { + switch vi.Spec.Storage { + case v1alpha2.StorageContainerRegistry: + return "DVCR" + case v1alpha2.StoragePersistentVolumeClaim: + return "PVC" + default: + return string(vi.Spec.Storage) + } +} diff --git a/test/e2e/default_config.yaml b/test/e2e/default_config.yaml index a5a34725d0..424a122c90 100644 --- a/test/e2e/default_config.yaml +++ b/test/e2e/default_config.yaml @@ -48,7 +48,6 @@ regexpLogFilter: - "failed to detach: .* not found" # "err" "failed to detach: virtualmachine.kubevirt.io \"head-497d17b-vm-automatic-with-hotplug\" not found", - "error patching .* not found" # "err" "error patching *** virtualimages.virtualization.deckhouse.io \"head-497d17b-vi-pvc-oref-vi-oref-vd\" not found", - "failed to get vmSnapshot: VirtualMachineSnapshot\\.virtualization\\.deckhouse.io .* not found" # "msg": "failed to get vmSnapshot: VirtualMachineSnapshot.virtualization.deckhouse.io \"main-to-pr14969-ynv-0-ef17ba-20250908-142437\" not found" - - "failed to sync virtual disk data source objectref: start immediate: internalvirtualizationdatavolumes.cdi.internal.virtualization.deckhouse.io .* is forbidden: unable to create new content in namespace .* because it is being terminated" # "err": "failed to sync virtual disk data source objectref: start immediate: internalvirtualizationdatavolumes.cdi.internal.virtualization.deckhouse.io \"vd-head-b3d8865-vd-root-migration-bios-d77ea313-f469-463d-a71b-00c89ca542ab\" is forbidden: unable to create new content in namespace head-b3d8865-end-to-end-vm-migration because it is being terminated" - "Failed to update lock optimistically:.*leases.*leader-election-helper.*" # error during virtualization-controller lifecycle: attempt to reacquire leader election. "msg": "Failed to update lock optimistically: Put \"http://127.0.0.1:23915/apis/coordination.k8s.io/v1/namespaces/d8-virtualization/leases/d8-virt-operator-leader-election-helper?timeout=5s\": context deadline exceeded (Client.Timeout exceeded while awaiting headers), falling back to slow path" - "Failed to update lock: .* leases.*leader-election-helper.*" # "msg": "ock: Operation cannot be fulfilled on leases.coordination.k8s.io \"d8-virt-operator-leader-election-helper\": the object has been modified; please apply your changes to the latest version and try again", - "error retrieving resource lock .*leader-election-helper" # "msg": "error retrieving resource lock d8-virtualization/d8-virt-operator-leader-election-helper: context deadline exceeded", diff --git a/test/e2e/internal/observer/cvi/observer.go b/test/e2e/internal/observer/cvi/observer.go new file mode 100644 index 0000000000..ef317cd04c --- /dev/null +++ b/test/e2e/internal/observer/cvi/observer.go @@ -0,0 +1,54 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package cvi provides a ClusterVirtualImage-specialized observer. +package cvi + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/observer" +) + +type Observer = observer.Observer[*v1alpha2.ClusterVirtualImage] + +type Predicate = observer.Predicate[*v1alpha2.ClusterVirtualImage] + +func StartObserver(ctx context.Context, f *framework.Framework, cvi *v1alpha2.ClusterVirtualImage) Observer { + GinkgoHelper() + + obs, err := observer.New[*v1alpha2.ClusterVirtualImage]( + ctx, + f.VirtClient().ClusterVirtualImages(), + cvi.Name, + cvi.Namespace, + ) + Expect(err).NotTo(HaveOccurred(), "failed to start observer for ClusterVirtualImage %s", cvi.Name) + + DeferCleanup(func() { + obs.Stop() + Expect(obs.Err()).NotTo(HaveOccurred(), + "ClusterVirtualImage %s observer reported an invariant violation", + cvi.Name) + }) + + return obs +} diff --git a/test/e2e/internal/observer/cvi/predicate.go b/test/e2e/internal/observer/cvi/predicate.go new file mode 100644 index 0000000000..2240e12cb9 --- /dev/null +++ b/test/e2e/internal/observer/cvi/predicate.go @@ -0,0 +1,106 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cvi + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +func BeFailed() Predicate { + return func(i *v1alpha2.ClusterVirtualImage) (bool, error) { + if i.Status.Phase == v1alpha2.ImageFailed { + return true, fmt.Errorf("ClusterVirtualImage entered Failed phase") + } + if cond := findCondition(i.Status.Conditions, cvicondition.ReadyType.String()); cond != nil { + if isConditionFresh(cond, i) && cond.Reason == cvicondition.ProvisioningFailed.String() { + return true, fmt.Errorf("ready condition reports ProvisioningFailed: %s", cond.Message) + } + } + return false, nil + } +} + +func BeReady() Predicate { + return func(i *v1alpha2.ClusterVirtualImage) (bool, error) { + readyCond := findCondition(i.Status.Conditions, cvicondition.ReadyType.String()) + + condStale := readyCond != nil && !isConditionFresh(readyCond, i) + condIsReady := readyCond != nil && + !condStale && + readyCond.Status == metav1.ConditionTrue && + readyCond.Reason == cvicondition.Ready.String() + phaseIsReady := i.Status.Phase == v1alpha2.ImageReady + + switch { + case phaseIsReady && condStale: + return false, nil + case phaseIsReady && !condIsReady: + return false, fmt.Errorf( + "phase is Ready but Ready condition is %s/%s (message: %q), expected True/%s", + condStatus(readyCond), condReason(readyCond), condMessage(readyCond), cvicondition.Ready, + ) + case condIsReady && !phaseIsReady: + return false, fmt.Errorf( + "ready condition is True/%s but phase is %q, expected %q", + cvicondition.Ready, i.Status.Phase, v1alpha2.ImageReady, + ) + case !phaseIsReady: + return false, nil + } + + return true, nil + } +} + +func isConditionFresh(cond *metav1.Condition, i *v1alpha2.ClusterVirtualImage) bool { + return cond.ObservedGeneration == i.GetGeneration() +} + +func findCondition(conds []metav1.Condition, condType string) *metav1.Condition { + for i := range conds { + if conds[i].Type == condType { + return &conds[i] + } + } + return nil +} + +func condStatus(cond *metav1.Condition) metav1.ConditionStatus { + if cond == nil { + return "" + } + return cond.Status +} + +func condReason(cond *metav1.Condition) string { + if cond == nil { + return "" + } + return cond.Reason +} + +func condMessage(cond *metav1.Condition) string { + if cond == nil { + return "" + } + return cond.Message +} diff --git a/test/e2e/internal/observer/observer.go b/test/e2e/internal/observer/observer.go new file mode 100644 index 0000000000..78a408ae21 --- /dev/null +++ b/test/e2e/internal/observer/observer.go @@ -0,0 +1,328 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package observer provides a generic, watch-based utility for monitoring the +// lifecycle of a single Kubernetes resource. +// +// The observer subscribes to events for a particular (name, namespace) pair +// and exposes three primitives: +// +// - WaitFor blocks until the predicate is satisfied by an event observed +// after the call, returns the predicate's diagnostic error, the timeout +// elapses, or one of the registered Always/Never invariants fires. +// - Never and Always register live predicates that are evaluated against +// every event recorded after the call, directly inside the watch loop. +// The very first violation is captured in Err and aborts every WaitFor +// currently in flight. +// +// Predicates use the [Predicate] signature, returning a (bool, error) tuple: +// +// - (true, nil) - the predicate is satisfied by the current state. +// - (false, nil) - the predicate is not satisfied yet. WaitFor keeps +// waiting; an Always invariant treats this as a violation; a Never +// invariant treats this as ok. +// - (_, err) - the predicate detected a definite, irrecoverable +// inconsistency. The error is propagated to WaitFor or used as the +// diagnostic for an Always/Never invariant violation. +package observer + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" +) + +// Watcher is the minimum interface required to start a watch on a Kubernetes +// resource. It matches typed clients generated by client-gen and the helpers +// already used by the e2e suite. +type Watcher interface { + Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) +} + +// Object is the type constraint for resources that can be observed. The +// observed type must be a pointer that implements metav1.Object so that the +// observer can filter watch events by name and namespace. +type Object interface { + metav1.Object +} + +// Predicate evaluates an observed state of T. See the package documentation +// for the meaning of the (bool, error) return value. +type Predicate[T Object] func(T) (bool, error) + +// Observer monitors a single Kubernetes resource through a watch and exposes +// invariant helpers over the sequence of states it observes. +// +// Implementations are safe for concurrent use. +type Observer[T Object] interface { + // Never registers a forbidden predicate. While the observer is running, + // every event is evaluated; the first time the predicate returns + // (true, _) or (_, err) the observer captures the violation in Err and + // aborts every WaitFor currently in flight. Never returns immediately and + // does not consult past history: only events recorded after the call are + // evaluated. + Never(predicate Predicate[T]) + // Always registers an invariant predicate. While the observer is + // running, every event is evaluated; the first time the predicate + // returns (false, _) or (_, err) the observer captures the violation in + // Err and aborts every WaitFor currently in flight. Always returns + // immediately and does not consult past history: only events recorded + // after the call are evaluated. + Always(predicate Predicate[T]) + // WaitFor returns nil as soon as the predicate returns (true, nil) for + // an event observed after the call. It returns an error if the predicate + // returns a non-nil error, the timeout elapses, the observer is stopped, + // or one of the registered invariants is violated. WaitFor does not + // consult past history. + WaitFor(predicate Predicate[T], timeout time.Duration) error + // Err returns the first invariant violation captured by Never or Always, + // or nil if no invariant has been violated. Once Err returns a non-nil + // error, no further events are evaluated against any registered + // invariant. + Err() error + // Stop terminates the underlying watch and unblocks every pending WaitFor + // call. Stop is idempotent and safe to call concurrently from multiple + // goroutines. + Stop() +} + +// New starts a watch via w and observes events for the resource identified by +// (name, namespace). The returned Observer is already running; the caller must +// invoke Stop to release the underlying watch. +// +// The watch is started before any caller-visible action takes place, which +// makes the function suitable for capturing the very first events emitted for +// a resource that the test is about to create. +func New[T Object]( + parentCtx context.Context, + w Watcher, + name, namespace string, +) (Observer[T], error) { + if w == nil { + return nil, errors.New("observer: watcher is nil") + } + + ctx, cancel := context.WithCancel(parentCtx) + wi, err := w.Watch(ctx, metav1.ListOptions{}) + if err != nil { + cancel() + return nil, fmt.Errorf("observer: start watch for %s/%s: %w", namespace, name, err) + } + + o := &observer[T]{ + name: name, + namespace: namespace, + listeners: make(map[chan T]struct{}), + invariantViolated: make(chan struct{}), + stop: make(chan struct{}), + done: make(chan struct{}), + } + + go o.run(wi, cancel) + + return o, nil +} + +type observer[T Object] struct { + name string + namespace string + + mu sync.Mutex + listeners map[chan T]struct{} + + invMu sync.Mutex + neverPredicates []Predicate[T] + alwaysPredicates []Predicate[T] + firstErr error + invariantViolated chan struct{} + invariantOnce sync.Once + + stop chan struct{} + stopOnce sync.Once + done chan struct{} +} + +func (o *observer[T]) run(wi watch.Interface, cancel context.CancelFunc) { + defer close(o.done) + defer wi.Stop() + defer cancel() + + for { + select { + case <-o.stop: + return + case event, ok := <-wi.ResultChan(): + if !ok { + return + } + obj, ok := event.Object.(T) + if !ok { + continue + } + if obj.GetName() != o.name || obj.GetNamespace() != o.namespace { + continue + } + o.broadcast(obj) + o.checkInvariants(obj) + } + } +} + +// broadcast forwards the event to every active WaitFor listener. +func (o *observer[T]) broadcast(obj T) { + o.mu.Lock() + defer o.mu.Unlock() + + for ch := range o.listeners { + select { + case ch <- obj: + default: + // Listener buffer is full; drop the event for this listener. + // WaitFor predicates are typically simple and never block, so + // this branch should not be reached in practice. + } + } +} + +// checkInvariants evaluates every registered Never/Always predicate against +// the latest observation. The first violation latches firstErr and closes +// invariantViolated, which causes any WaitFor currently in flight to abort +// with the captured error. +// +// Predicates are evaluated sequentially inside the watch goroutine so that +// stateful closures see events in the order they were emitted by the API +// server. After the first violation no further predicates are evaluated for +// any subsequent event. +func (o *observer[T]) checkInvariants(obj T) { + o.invMu.Lock() + defer o.invMu.Unlock() + + if o.firstErr != nil { + return + } + + for _, p := range o.neverPredicates { + ok, err := p(obj) + if err != nil { + o.firstErr = fmt.Errorf("observer: Never predicate: %w", err) + o.signalViolationLocked() + return + } + if ok { + o.firstErr = errors.New("observer: Never predicate matched a live observation") + o.signalViolationLocked() + return + } + } + for _, p := range o.alwaysPredicates { + ok, err := p(obj) + if err != nil { + o.firstErr = fmt.Errorf("observer: Always predicate: %w", err) + o.signalViolationLocked() + return + } + if !ok { + o.firstErr = errors.New("observer: Always predicate did not hold for a live observation") + o.signalViolationLocked() + return + } + } +} + +// signalViolationLocked closes the invariantViolated channel exactly once. +// Must be called with invMu held. +func (o *observer[T]) signalViolationLocked() { + o.invariantOnce.Do(func() { + close(o.invariantViolated) + }) +} + +func (o *observer[T]) Never(predicate Predicate[T]) { + o.invMu.Lock() + defer o.invMu.Unlock() + o.neverPredicates = append(o.neverPredicates, predicate) +} + +func (o *observer[T]) Always(predicate Predicate[T]) { + o.invMu.Lock() + defer o.invMu.Unlock() + o.alwaysPredicates = append(o.alwaysPredicates, predicate) +} + +func (o *observer[T]) Err() error { + o.invMu.Lock() + defer o.invMu.Unlock() + return o.firstErr +} + +func (o *observer[T]) WaitFor(predicate Predicate[T], timeout time.Duration) error { + // If an invariant has already fired, abort immediately. + if err := o.Err(); err != nil { + return fmt.Errorf("observer: WaitFor aborted by invariant: %w", err) + } + + // If the observer has already been stopped, no future events will arrive. + select { + case <-o.done: + return errors.New("observer: WaitFor: observer is stopped") + default: + } + + o.mu.Lock() + ch := make(chan T, 256) + o.listeners[ch] = struct{}{} + o.mu.Unlock() + + defer func() { + o.mu.Lock() + delete(o.listeners, ch) + o.mu.Unlock() + }() + + timer := time.NewTimer(timeout) + defer timer.Stop() + + for { + select { + case obj := <-ch: + ok, err := predicate(obj) + if err != nil { + return fmt.Errorf("observer: WaitFor predicate: %w", err) + } + if ok { + return nil + } + case <-timer.C: + return fmt.Errorf("observer: WaitFor timed out after %s", timeout) + case <-o.done: + return errors.New("observer: WaitFor: observer stopped before predicate became true") + case <-o.invariantViolated: + return fmt.Errorf("observer: WaitFor aborted by invariant: %w", o.Err()) + } + } +} + +func (o *observer[T]) Stop() { + o.stopOnce.Do(func() { + close(o.stop) + }) + <-o.done +} diff --git a/test/e2e/internal/observer/vd/observer.go b/test/e2e/internal/observer/vd/observer.go new file mode 100644 index 0000000000..52c70edfb1 --- /dev/null +++ b/test/e2e/internal/observer/vd/observer.go @@ -0,0 +1,70 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package vd provides a VirtualDisk-specialized [observer.Observer] together +// with a curated set of predicates ready to be used with its Never, Always +// and WaitFor primitives. +package vd + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/observer" +) + +// Observer is a convenience type alias for the generic Observer specialized +// for VirtualDisks. +type Observer = observer.Observer[*v1alpha2.VirtualDisk] + +// Predicate is a convenience type alias for the generic Predicate specialized +// for VirtualDisks. +type Predicate = observer.Predicate[*v1alpha2.VirtualDisk] + +// StartObserver starts a VirtualDisk Observer for the given disk and +// registers a DeferCleanup that: +// +// 1. stops the underlying watch, releasing the watcher resources; +// 2. asserts that no Never/Always invariant registered on the observer was +// violated during the test. +// +// The watch is started before the caller creates the VirtualDisk, ensuring +// that the very first phase transitions are captured and that any live +// invariants registered on the returned observer see every emitted event. +func StartObserver(ctx context.Context, f *framework.Framework, vd *v1alpha2.VirtualDisk) Observer { + GinkgoHelper() + + obs, err := observer.New[*v1alpha2.VirtualDisk]( + ctx, + f.VirtClient().VirtualDisks(vd.Namespace), + vd.Name, + vd.Namespace, + ) + Expect(err).NotTo(HaveOccurred(), "failed to start observer for VirtualDisk %s/%s", vd.Namespace, vd.Name) + + DeferCleanup(func() { + obs.Stop() + Expect(obs.Err()).NotTo(HaveOccurred(), + "VirtualDisk %s/%s observer reported an invariant violation", + vd.Namespace, vd.Name) + }) + + return obs +} diff --git a/test/e2e/internal/observer/vd/predicate.go b/test/e2e/internal/observer/vd/predicate.go new file mode 100644 index 0000000000..9cb49af450 --- /dev/null +++ b/test/e2e/internal/observer/vd/predicate.go @@ -0,0 +1,274 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vd + +import ( + "errors" + "fmt" + "strconv" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +// readyProgress is the value of VirtualDisk.Status.Progress when the disk +// has finished provisioning. +const readyProgress = "100%" + +// BeFailed reports an invariant violation when the VirtualDisk has reached +// the terminal Failed phase or its Ready condition reports the +// ProvisioningFailed reason. It is intended to be used with [Observer.Never]. +func BeFailed() Predicate { + return func(d *v1alpha2.VirtualDisk) (bool, error) { + if d.Status.Phase == v1alpha2.DiskFailed { + return true, fmt.Errorf("VirtualDisk entered Failed phase") + } + if cond := findCondition(d.Status.Conditions, vdcondition.ReadyType.String()); cond != nil { + if isConditionFresh(cond, d) && cond.Reason == vdcondition.ProvisioningFailed.String() { + return true, fmt.Errorf("ready condition reports ProvisioningFailed: %s", cond.Message) + } + } + return false, nil + } +} + +// BeStorageClassReady reports the StorageClassReady condition is healthy. +// +// The condition is treated as healthy when: +// - it is absent (the controller has not yet computed it); +// - it is stale, i.e. its observedGeneration does not match the resource +// generation (the test should wait for the controller to refresh it); +// - it has Status=True with Reason=StorageClassReady. +// +// Any other state is reported as a definite invariant violation. Intended +// for use with [Observer.Always]. +func BeStorageClassReady() Predicate { + return func(d *v1alpha2.VirtualDisk) (bool, error) { + cond := findCondition(d.Status.Conditions, vdcondition.StorageClassReadyType.String()) + if cond == nil || !isConditionFresh(cond, d) { + return true, nil + } + if cond.Status != metav1.ConditionTrue { + return false, fmt.Errorf( + "StorageClassReady condition is %s/%s (message: %q), expected True/%s", + cond.Status, cond.Reason, cond.Message, vdcondition.StorageClassReady, + ) + } + if cond.Reason != vdcondition.StorageClassReady.String() { + return false, fmt.Errorf( + "StorageClassReady reason is %q, expected %q", + cond.Reason, vdcondition.StorageClassReady, + ) + } + return true, nil + } +} + +// BeDataSourceReady reports the DatasourceReady condition is healthy. +// +// The condition is treated as healthy under the same rules as for +// [BeStorageClassReady] (absent, stale, or True/DatasourceReady). The +// controller legitimately removes this condition once the disk has reached +// the Ready phase, so an absent condition is always treated as healthy. +// Intended for use with [Observer.Always]. +func BeDataSourceReady() Predicate { + return func(d *v1alpha2.VirtualDisk) (bool, error) { + cond := findCondition(d.Status.Conditions, vdcondition.DatasourceReadyType.String()) + if cond == nil || !isConditionFresh(cond, d) { + return true, nil + } + if cond.Status != metav1.ConditionTrue { + return false, fmt.Errorf( + "DatasourceReady condition is %s/%s (message: %q), expected True/%s", + cond.Status, cond.Reason, cond.Message, vdcondition.DatasourceReady, + ) + } + if cond.Reason != vdcondition.DatasourceReady.String() { + return false, fmt.Errorf( + "DatasourceReady reason is %q, expected %q", + cond.Reason, vdcondition.DatasourceReady, + ) + } + return true, nil + } +} + +// BeReady reports the VirtualDisk is fully provisioned. +// +// The predicate is satisfied only when the phase, the Ready condition, the +// progress, the capacity, the target PVC name and the storage class name are +// all populated and consistent with each other. Intended for use with +// [Observer.WaitFor]. +// +// Returned values: +// - (true, nil) - the disk is ready and every status field is populated; +// - (false, nil) - the disk is still being provisioned or the Ready +// condition is stale; +// - (false, err) - the disk reports an internally inconsistent ready state +// (phase Ready without a matching Ready condition, or with a missing +// status field). The error fails the WaitFor immediately. +func BeReady() Predicate { + return func(d *v1alpha2.VirtualDisk) (bool, error) { + readyCond := findCondition(d.Status.Conditions, vdcondition.ReadyType.String()) + + condStale := readyCond != nil && !isConditionFresh(readyCond, d) + condIsReady := readyCond != nil && + !condStale && + readyCond.Status == metav1.ConditionTrue && + readyCond.Reason == vdcondition.Ready.String() + phaseIsReady := d.Status.Phase == v1alpha2.DiskReady + + switch { + case phaseIsReady && condStale: + // Wait for the controller to refresh the Ready condition. + return false, nil + case phaseIsReady && !condIsReady: + return false, fmt.Errorf( + "phase is Ready but Ready condition is %s/%s (message: %q), expected True/%s", + condStatus(readyCond), condReason(readyCond), condMessage(readyCond), vdcondition.Ready, + ) + case condIsReady && !phaseIsReady: + return false, fmt.Errorf( + "ready condition is True/%s but phase is %q, expected %q", + vdcondition.Ready, d.Status.Phase, v1alpha2.DiskReady, + ) + case !phaseIsReady: + return false, nil + } + + if d.Status.Progress != readyProgress { + return false, fmt.Errorf( + "phase is Ready but progress is %q, expected %q", + d.Status.Progress, readyProgress, + ) + } + if d.Status.Capacity == "" { + return false, errors.New("phase is Ready but capacity is empty") + } + if d.Status.Target.PersistentVolumeClaim == "" { + return false, errors.New("phase is Ready but target.persistentVolumeClaimName is empty") + } + if d.Status.StorageClassName == "" { + return false, errors.New("phase is Ready but storageClassName is empty") + } + + return true, nil + } +} + +// BeReadyForUserUpload reports the VirtualDisk has reached the +// WaitForUserUpload phase and exposes a usable external upload URL. +// +// Returned values: +// - (true, nil) - the disk is in WaitForUserUpload and has both upload +// URLs populated; +// - (false, nil) - the disk has not yet reached WaitForUserUpload; +// - (false, err) - the disk is in WaitForUserUpload but the upload URLs +// are missing or empty (a controller bug). +func BeReadyForUserUpload() Predicate { + return func(d *v1alpha2.VirtualDisk) (bool, error) { + if d.Status.Phase != v1alpha2.DiskWaitForUserUpload { + return false, nil + } + if d.Status.ImageUploadURLs == nil { + return false, errors.New("phase is WaitForUserUpload but ImageUploadURLs is nil") + } + if d.Status.ImageUploadURLs.External == "" { + return false, errors.New("phase is WaitForUserUpload but external upload URL is empty") + } + return true, nil + } +} + +// HaveNonDecreasingProgress reports an invariant violation when +// VirtualDisk.Status.Progress moves backwards between observed states. +func HaveNonDecreasingProgress() Predicate { + var previous *float64 + + return func(d *v1alpha2.VirtualDisk) (bool, error) { + if d.Status.Progress == "" { + return true, nil + } + + current, err := parseProgress(d.Status.Progress) + if err != nil { + return false, err + } + + if previous != nil && current < *previous { + return false, fmt.Errorf("progress decreased from %.2f%% to %.2f%%", *previous, current) + } + + previous = ¤t + return true, nil + } +} + +func parseProgress(progress string) (float64, error) { + value := strings.TrimSuffix(progress, "%") + if value == progress { + return 0, fmt.Errorf("progress %q does not have %% suffix", progress) + } + + parsed, err := strconv.ParseFloat(value, 64) + if err != nil { + return 0, fmt.Errorf("parse progress %q: %w", progress, err) + } + if parsed < 0 || parsed > 100 { + return 0, fmt.Errorf("progress %q is outside 0..100 range", progress) + } + return parsed, nil +} + +// isConditionFresh reports whether the condition has been computed against +// the latest observed generation of the resource. +func isConditionFresh(cond *metav1.Condition, d *v1alpha2.VirtualDisk) bool { + return cond.ObservedGeneration == d.GetGeneration() +} + +func findCondition(conds []metav1.Condition, condType string) *metav1.Condition { + for i := range conds { + if conds[i].Type == condType { + return &conds[i] + } + } + return nil +} + +func condStatus(cond *metav1.Condition) metav1.ConditionStatus { + if cond == nil { + return "" + } + return cond.Status +} + +func condReason(cond *metav1.Condition) string { + if cond == nil { + return "" + } + return cond.Reason +} + +func condMessage(cond *metav1.Condition) string { + if cond == nil { + return "" + } + return cond.Message +} diff --git a/test/e2e/internal/observer/vi/observer.go b/test/e2e/internal/observer/vi/observer.go new file mode 100644 index 0000000000..dc4ae316c0 --- /dev/null +++ b/test/e2e/internal/observer/vi/observer.go @@ -0,0 +1,70 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package vi provides a VirtualImage-specialized [observer.Observer] together +// with a curated set of predicates ready to be used with its Never, Always +// and WaitFor primitives. +package vi + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/test/e2e/internal/framework" + "github.com/deckhouse/virtualization/test/e2e/internal/observer" +) + +// Observer is a convenience type alias for the generic Observer specialized +// for VirtualImages. +type Observer = observer.Observer[*v1alpha2.VirtualImage] + +// Predicate is a convenience type alias for the generic Predicate specialized +// for VirtualImages. +type Predicate = observer.Predicate[*v1alpha2.VirtualImage] + +// StartObserver starts a VirtualImage Observer for the given image and +// registers a DeferCleanup that: +// +// 1. stops the underlying watch, releasing the watcher resources; +// 2. asserts that no Never/Always invariant registered on the observer was +// violated during the test. +// +// The watch is started before the caller creates the VirtualImage, ensuring +// that the very first phase transitions are captured and that any live +// invariants registered on the returned observer see every emitted event. +func StartObserver(ctx context.Context, f *framework.Framework, vi *v1alpha2.VirtualImage) Observer { + GinkgoHelper() + + obs, err := observer.New[*v1alpha2.VirtualImage]( + ctx, + f.VirtClient().VirtualImages(vi.Namespace), + vi.Name, + vi.Namespace, + ) + Expect(err).NotTo(HaveOccurred(), "failed to start observer for VirtualImage %s/%s", vi.Namespace, vi.Name) + + DeferCleanup(func() { + obs.Stop() + Expect(obs.Err()).NotTo(HaveOccurred(), + "VirtualImage %s/%s observer reported an invariant violation", + vi.Namespace, vi.Name) + }) + + return obs +} diff --git a/test/e2e/internal/observer/vi/predicate.go b/test/e2e/internal/observer/vi/predicate.go new file mode 100644 index 0000000000..3426db47ae --- /dev/null +++ b/test/e2e/internal/observer/vi/predicate.go @@ -0,0 +1,176 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vi + +import ( + "errors" + "fmt" + "strconv" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +// BeFailed reports an invariant violation when the VirtualImage has reached +// the terminal Failed phase or its Ready condition reports the +// ProvisioningFailed reason. It is intended to be used with [Observer.Never]. +func BeFailed() Predicate { + return func(i *v1alpha2.VirtualImage) (bool, error) { + if i.Status.Phase == v1alpha2.ImageFailed { + return true, fmt.Errorf("VirtualImage entered Failed phase") + } + if cond := findCondition(i.Status.Conditions, vicondition.ReadyType.String()); cond != nil { + if isConditionFresh(cond, i) && cond.Reason == vicondition.ProvisioningFailed.String() { + return true, fmt.Errorf("ready condition reports ProvisioningFailed: %s", cond.Message) + } + } + return false, nil + } +} + +// BeReady reports the VirtualImage has finished provisioning. +// +// The predicate is satisfied only when the phase is Ready and the Ready +// condition is True/Ready and is not stale. Inconsistencies (phase Ready +// without a fresh Ready condition matching it) produce a (false, error) +// pair so that any WaitFor caller fails immediately. Intended for use with +// [Observer.WaitFor]. +func BeReady() Predicate { + return func(i *v1alpha2.VirtualImage) (bool, error) { + readyCond := findCondition(i.Status.Conditions, vicondition.ReadyType.String()) + + condStale := readyCond != nil && !isConditionFresh(readyCond, i) + condIsReady := readyCond != nil && + !condStale && + readyCond.Status == metav1.ConditionTrue && + readyCond.Reason == vicondition.Ready.String() + phaseIsReady := i.Status.Phase == v1alpha2.ImageReady + + switch { + case phaseIsReady && condStale: + return false, nil + case phaseIsReady && !condIsReady: + return false, fmt.Errorf( + "phase is Ready but Ready condition is %s/%s (message: %q), expected True/%s", + condStatus(readyCond), condReason(readyCond), condMessage(readyCond), vicondition.Ready, + ) + case condIsReady && !phaseIsReady: + return false, fmt.Errorf( + "ready condition is True/%s but phase is %q, expected %q", + vicondition.Ready, i.Status.Phase, v1alpha2.ImageReady, + ) + case !phaseIsReady: + return false, nil + } + + return true, nil + } +} + +// BeReadyForUserUpload reports the VirtualImage has reached the +// WaitForUserUpload phase and exposes a usable external upload URL. +func BeReadyForUserUpload() Predicate { + return func(i *v1alpha2.VirtualImage) (bool, error) { + if i.Status.Phase != v1alpha2.ImageWaitForUserUpload { + return false, nil + } + if i.Status.ImageUploadURLs == nil { + return false, errors.New("phase is WaitForUserUpload but ImageUploadURLs is nil") + } + if i.Status.ImageUploadURLs.External == "" { + return false, errors.New("phase is WaitForUserUpload but external upload URL is empty") + } + return true, nil + } +} + +// HaveNonDecreasingProgress reports an invariant violation when +// VirtualImage.Status.Progress moves backwards between observed states. +func HaveNonDecreasingProgress() Predicate { + var previous *float64 + + return func(i *v1alpha2.VirtualImage) (bool, error) { + if i.Status.Progress == "" { + return true, nil + } + + current, err := parseProgress(i.Status.Progress) + if err != nil { + return false, err + } + + if previous != nil && current < *previous { + return false, fmt.Errorf("progress decreased from %.2f%% to %.2f%%", *previous, current) + } + + previous = ¤t + return true, nil + } +} + +func parseProgress(progress string) (float64, error) { + value := strings.TrimSuffix(progress, "%") + if value == progress { + return 0, fmt.Errorf("progress %q does not have %% suffix", progress) + } + + parsed, err := strconv.ParseFloat(value, 64) + if err != nil { + return 0, fmt.Errorf("parse progress %q: %w", progress, err) + } + if parsed < 0 || parsed > 100 { + return 0, fmt.Errorf("progress %q is outside 0..100 range", progress) + } + return parsed, nil +} + +func isConditionFresh(cond *metav1.Condition, i *v1alpha2.VirtualImage) bool { + return cond.ObservedGeneration == i.GetGeneration() +} + +func findCondition(conds []metav1.Condition, condType string) *metav1.Condition { + for i := range conds { + if conds[i].Type == condType { + return &conds[i] + } + } + return nil +} + +func condStatus(cond *metav1.Condition) metav1.ConditionStatus { + if cond == nil { + return "" + } + return cond.Status +} + +func condReason(cond *metav1.Condition) string { + if cond == nil { + return "" + } + return cond.Reason +} + +func condMessage(cond *metav1.Condition) string { + if cond == nil { + return "" + } + return cond.Message +} diff --git a/test/e2e/internal/rewrite/types.go b/test/e2e/internal/rewrite/types.go index 69b7da3f9d..05b7d6dce1 100644 --- a/test/e2e/internal/rewrite/types.go +++ b/test/e2e/internal/rewrite/types.go @@ -22,9 +22,9 @@ import ( cdiv1beta1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" ) -func rewriteCDIV1beta1(resource string) schema.GroupVersionResource { +func rewriteVirtualizationV1beta1(resource string) schema.GroupVersionResource { return schema.GroupVersionResource{ - Group: "cdi.internal.virtualization.deckhouse.io", + Group: "internal.virtualization.deckhouse.io", Version: "v1beta1", Resource: resource, } @@ -48,7 +48,7 @@ type StorageProfile struct { func (StorageProfile) GVR() schema.GroupVersionResource { resource := rewriteInternalVirtualizationResource("storageprofiles") - return rewriteCDIV1beta1(resource) + return rewriteVirtualizationV1beta1(resource) } type VirtualMachineInstanceMigration struct { diff --git a/tools/kubeconform/fixtures/module-values.yaml b/tools/kubeconform/fixtures/module-values.yaml index 43a6b2e6d2..60592b355e 100644 --- a/tools/kubeconform/fixtures/module-values.yaml +++ b/tools/kubeconform/fixtures/module-values.yaml @@ -100,8 +100,7 @@ global: - deckhouse.io/v1alpha1/ClusterAuthorizationRule - deckhouse.io/v1alpha1/DexProvider - deckhouse.io/v1beta1/DeploymentMetric - - cdi.internal.virtualization.deckhouse.io/v1beta1/InternalVirtualizationCDI - - cdi.internal.virtualization.deckhouse.io/v1beta1/InternalVirtualizationDataVolume + - cdi.internal.virtualization.deckhouse.io/v1beta1/InternalVirtualizationStorageProfile - templates.gatekeeper.sh/v1alpha1/ConstraintTemplate - cilium.io/v2alpha1/CiliumNodeConfig - deckhouse.io/v1alpha1/LocalPathProvisioner @@ -333,6 +332,7 @@ global: vmRouteForge: sha256:0000000000000000000000000000000000000000000000000000000000000000 virtualizationDraUsb: sha256:0000000000000000000000000000000000000000000000000000000000000000 usbModules: sha256:0000000000000000000000000000000000000000000000000000000000000000 + virtualDiskImporter: sha256:0000000000000000000000000000000000000000000000000000000000000000 registry: CA: "" address: some-registry.io