mirror of
https://github.com/lukaszraczylo/kubernetes-images-sync-operator.git
synced 2026-07-06 03:39:42 +00:00
Clean up the code and basic improvements.
This commit is contained in:
@@ -6,9 +6,9 @@ CHART_DIR = charts/kube-images-sync-operator
|
|||||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||||
ENVTEST_K8S_VERSION = 1.31.0
|
ENVTEST_K8S_VERSION = 1.31.0
|
||||||
|
|
||||||
CURRENT_VERSION = $(shell semver-gen generate -l | awk '/^SEMVER/ {print $$NF}')
|
CURRENT_VERSION ?= $(shell semver-gen generate -l 2>/dev/null | awk '/^SEMVER/ {print $$NF}')
|
||||||
ifeq ($(CURRENT_VERSION),)
|
ifeq ($(CURRENT_VERSION),)
|
||||||
$(error Failed to extract version number)
|
CURRENT_VERSION = 0.5.54
|
||||||
endif
|
endif
|
||||||
|
|
||||||
IMAGE_VERSION_TAG ?= $(CURRENT_VERSION)
|
IMAGE_VERSION_TAG ?= $(CURRENT_VERSION)
|
||||||
@@ -174,9 +174,9 @@ HELMIFY ?= helmify
|
|||||||
|
|
||||||
## Tool Versions
|
## Tool Versions
|
||||||
KUSTOMIZE_VERSION ?= v5.4.3
|
KUSTOMIZE_VERSION ?= v5.4.3
|
||||||
CONTROLLER_TOOLS_VERSION ?= v0.16.1
|
CONTROLLER_TOOLS_VERSION ?= v0.17.1
|
||||||
ENVTEST_VERSION ?= release-0.19
|
ENVTEST_VERSION ?= release-0.19
|
||||||
GOLANGCI_LINT_VERSION ?= v1.59.1
|
GOLANGCI_LINT_VERSION ?= v1.62.2
|
||||||
|
|
||||||
.PHONY: print-version
|
.PHONY: print-version
|
||||||
print-version:
|
print-version:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type ClusterImageStorageS3 struct {
|
|||||||
|
|
||||||
// ClusterImageStorageSpec defines the desired state of ClusterImageStorage
|
// ClusterImageStorageSpec defines the desired state of ClusterImageStorage
|
||||||
type ClusterImageStorageSpec struct {
|
type ClusterImageStorageSpec struct {
|
||||||
// +kubebuilder:validation:Enum=file;S3
|
// +kubebuilder:validation:Enum=FILE;S3
|
||||||
StorageTarget string `json:"target"`
|
StorageTarget string `json:"target"`
|
||||||
S3 ClusterImageStorageS3 `json:"s3,omitempty"`
|
S3 ClusterImageStorageS3 `json:"s3,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -67,8 +67,9 @@ type ClusterImageExportSpec struct {
|
|||||||
JobAnnotations map[string]string `json:"jobAnnotations,omitempty"`
|
JobAnnotations map[string]string `json:"jobAnnotations,omitempty"`
|
||||||
// +kubebuilder:validation:Optional
|
// +kubebuilder:validation:Optional
|
||||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||||
// +kubebuilder:validation.Minimum=1
|
// +kubebuilder:validation:Minimum=1
|
||||||
// +kubebuilder:validation.Maximum=100
|
// +kubebuilder:validation:Maximum=100
|
||||||
|
// +kubebuilder:default=5
|
||||||
MaxConcurrentJobs int `json:"maxConcurrentJobs"`
|
MaxConcurrentJobs int `json:"maxConcurrentJobs"`
|
||||||
AdditionalImages []string `json:"additionalImages,omitempty"`
|
AdditionalImages []string `json:"additionalImages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ kind: CustomResourceDefinition
|
|||||||
metadata:
|
metadata:
|
||||||
name: clusterimages.raczylo.com
|
name: clusterimages.raczylo.com
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.16.1
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
labels:
|
labels:
|
||||||
{{- include "chart.labels" . | nindent 4 }}
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
@@ -124,9 +124,3 @@ spec:
|
|||||||
storage: true
|
storage: true
|
||||||
subresources:
|
subresources:
|
||||||
status: {}
|
status: {}
|
||||||
status:
|
|
||||||
acceptedNames:
|
|
||||||
kind: ""
|
|
||||||
plural: ""
|
|
||||||
conditions: []
|
|
||||||
storedVersions: []
|
|
||||||
@@ -3,7 +3,7 @@ kind: CustomResourceDefinition
|
|||||||
metadata:
|
metadata:
|
||||||
name: clusterimageexports.raczylo.com
|
name: clusterimageexports.raczylo.com
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.16.1
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
labels:
|
labels:
|
||||||
{{- include "chart.labels" . | nindent 4 }}
|
{{- include "chart.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
@@ -37,7 +37,8 @@ spec:
|
|||||||
name: v1
|
name: v1
|
||||||
schema:
|
schema:
|
||||||
openAPIV3Schema:
|
openAPIV3Schema:
|
||||||
description: ClusterImageExport is the Schema for the clusterimageexports API
|
description: ClusterImageExport is the Schema for the clusterimageexports
|
||||||
|
API
|
||||||
properties:
|
properties:
|
||||||
apiVersion:
|
apiVersion:
|
||||||
description: |-
|
description: |-
|
||||||
@@ -108,6 +109,9 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
maxConcurrentJobs:
|
maxConcurrentJobs:
|
||||||
|
default: 5
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@@ -116,7 +120,8 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
storage:
|
storage:
|
||||||
description: ClusterImageStorageSpec defines the desired state of ClusterImageStorage
|
description: ClusterImageStorageSpec defines the desired state of
|
||||||
|
ClusterImageStorage
|
||||||
properties:
|
properties:
|
||||||
s3:
|
s3:
|
||||||
properties:
|
properties:
|
||||||
@@ -134,8 +139,8 @@ spec:
|
|||||||
region:
|
region:
|
||||||
type: string
|
type: string
|
||||||
roleARN:
|
roleARN:
|
||||||
description: RoleARN is the ARN of the role to be used for the
|
description: RoleARN is the ARN of the role to be used for
|
||||||
deployment
|
the deployment
|
||||||
type: string
|
type: string
|
||||||
secretKey:
|
secretKey:
|
||||||
type: string
|
type: string
|
||||||
@@ -150,7 +155,7 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
target:
|
target:
|
||||||
enum:
|
enum:
|
||||||
- file
|
- FILE
|
||||||
- S3
|
- S3
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
@@ -179,9 +184,3 @@ spec:
|
|||||||
storage: true
|
storage: true
|
||||||
subresources:
|
subresources:
|
||||||
status: {}
|
status: {}
|
||||||
status:
|
|
||||||
acceptedNames:
|
|
||||||
kind: ""
|
|
||||||
plural: ""
|
|
||||||
conditions: []
|
|
||||||
storedVersions: []
|
|
||||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
|||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.16.1
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
name: clusterimageexports.raczylo.com
|
name: clusterimageexports.raczylo.com
|
||||||
spec:
|
spec:
|
||||||
group: raczylo.com
|
group: raczylo.com
|
||||||
@@ -108,6 +108,9 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
maxConcurrentJobs:
|
maxConcurrentJobs:
|
||||||
|
default: 5
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@@ -151,7 +154,7 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
target:
|
target:
|
||||||
enum:
|
enum:
|
||||||
- file
|
- FILE
|
||||||
- S3
|
- S3
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
|||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.16.1
|
controller-gen.kubebuilder.io/version: v0.17.1
|
||||||
name: clusterimages.raczylo.com
|
name: clusterimages.raczylo.com
|
||||||
spec:
|
spec:
|
||||||
group: raczylo.com
|
group: raczylo.com
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
|
# This patch adds the args to allow exposing the metrics endpoint securely using HTTPS
|
||||||
- op: add
|
- op: add
|
||||||
path: /spec/template/spec/containers/0/args/0
|
path: /spec/template/spec/containers/0/args/0
|
||||||
value: --metrics-bind-address=:8443
|
value: --metrics-bind-address=:8443
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/containers/0/args/1
|
||||||
|
value: --metrics-secure
|
||||||
|
|||||||
+67
-68
@@ -4,71 +4,70 @@ kind: ClusterRole
|
|||||||
metadata:
|
metadata:
|
||||||
name: impex-mgr
|
name: impex-mgr
|
||||||
rules:
|
rules:
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
- pods
|
- pods
|
||||||
verbs:
|
verbs:
|
||||||
- create
|
- create
|
||||||
- delete
|
- delete
|
||||||
- deletecollection
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- apps
|
||||||
- apps
|
resources:
|
||||||
resources:
|
- daemonsets
|
||||||
- daemonsets
|
- deployments
|
||||||
- deployments
|
verbs:
|
||||||
verbs:
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- batch
|
||||||
- batch
|
resources:
|
||||||
resources:
|
- cronjobs
|
||||||
- cronjobs
|
verbs:
|
||||||
verbs:
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- batch
|
||||||
- batch
|
resources:
|
||||||
resources:
|
- jobs
|
||||||
- jobs
|
verbs:
|
||||||
verbs:
|
- create
|
||||||
- create
|
- delete
|
||||||
- delete
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- raczylo.com
|
||||||
- raczylo.com
|
resources:
|
||||||
resources:
|
- '*'
|
||||||
- "*"
|
verbs:
|
||||||
verbs:
|
- create
|
||||||
- create
|
- delete
|
||||||
- delete
|
- get
|
||||||
- get
|
- list
|
||||||
- list
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
- watch
|
||||||
- watch
|
- apiGroups:
|
||||||
- apiGroups:
|
- raczylo.com
|
||||||
- raczylo.com
|
resources:
|
||||||
resources:
|
- '*/finalizers'
|
||||||
- "*/finalizers"
|
verbs:
|
||||||
verbs:
|
- update
|
||||||
- update
|
- apiGroups:
|
||||||
- apiGroups:
|
- raczylo.com
|
||||||
- raczylo.com
|
resources:
|
||||||
resources:
|
- '*/status'
|
||||||
- "*/status"
|
verbs:
|
||||||
verbs:
|
- get
|
||||||
- get
|
- patch
|
||||||
- patch
|
- update
|
||||||
- update
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
@@ -13,8 +14,9 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/ptr"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
@@ -28,6 +30,7 @@ type ClusterImageReconciler struct {
|
|||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
MaxParallelJobs int
|
MaxParallelJobs int
|
||||||
ActiveJobs int
|
ActiveJobs int
|
||||||
|
activeJobsMu sync.Mutex // protects ActiveJobs counter
|
||||||
KubeClient *kubernetes.Clientset
|
KubeClient *kubernetes.Clientset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ func (r *ClusterImageReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
|||||||
if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
|
if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latest.Status.Progress = shared.STATUS_PENDING
|
latest.Status.Progress = shared.STATUS_PENDING
|
||||||
if err := r.Status().Update(ctx, latest); err != nil {
|
if err := r.Status().Update(ctx, latest); err != nil {
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
@@ -80,7 +83,10 @@ func (r *ClusterImageReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we've reached the maximum number of parallel jobs, requeue
|
// If we've reached the maximum number of parallel jobs, requeue
|
||||||
if r.ActiveJobs >= r.MaxParallelJobs && clusterImage.Status.Progress == shared.STATUS_PENDING {
|
r.activeJobsMu.Lock()
|
||||||
|
activeJobs := r.ActiveJobs
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
|
if activeJobs >= r.MaxParallelJobs && clusterImage.Status.Progress == shared.STATUS_PENDING {
|
||||||
return ctrl.Result{RequeueAfter: time.Second * 30}, nil
|
return ctrl.Result{RequeueAfter: time.Second * 30}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +123,7 @@ func (r *ClusterImageReconciler) handlePendingClusterImage(ctx context.Context,
|
|||||||
if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Name, Namespace: clusterImage.Namespace}, latest); err != nil {
|
if err := r.Get(ctx, types.NamespacedName{Name: clusterImage.Name, Namespace: clusterImage.Namespace}, latest); err != nil {
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latest.Status.Progress = shared.STATUS_PRESENT
|
latest.Status.Progress = shared.STATUS_PRESENT
|
||||||
if err := r.Status().Update(ctx, latest); err != nil {
|
if err := r.Status().Update(ctx, latest); err != nil {
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
@@ -151,7 +157,9 @@ func (r *ClusterImageReconciler) handlePendingClusterImage(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Increment the active jobs count
|
// Increment the active jobs count
|
||||||
|
r.activeJobsMu.Lock()
|
||||||
r.ActiveJobs++
|
r.ActiveJobs++
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
|
|
||||||
return ctrl.Result{Requeue: true}, nil
|
return ctrl.Result{Requeue: true}, nil
|
||||||
}
|
}
|
||||||
@@ -217,7 +225,9 @@ func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
latest.Status.Progress = shared.STATUS_SUCCESS
|
latest.Status.Progress = shared.STATUS_SUCCESS
|
||||||
|
r.activeJobsMu.Lock()
|
||||||
r.ActiveJobs--
|
r.ActiveJobs--
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
// Update the status before cleaning up the job
|
// Update the status before cleaning up the job
|
||||||
if err := r.Status().Update(ctx, latest); err != nil {
|
if err := r.Status().Update(ctx, latest); err != nil {
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
@@ -232,7 +242,9 @@ func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context,
|
|||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
} else if existingJob.Status.Failed > 0 {
|
} else if existingJob.Status.Failed > 0 {
|
||||||
|
r.activeJobsMu.Lock()
|
||||||
r.ActiveJobs--
|
r.ActiveJobs--
|
||||||
|
r.activeJobsMu.Unlock()
|
||||||
if clusterImage.Status.RetryCount < 3 {
|
if clusterImage.Status.RetryCount < 3 {
|
||||||
// Cleanup the failed job before retrying
|
// Cleanup the failed job before retrying
|
||||||
if err := r.cleanupJobAndPods(ctx, existingJob); err != nil {
|
if err := r.cleanupJobAndPods(ctx, existingJob); err != nil {
|
||||||
@@ -300,8 +312,22 @@ func (r *ClusterImageReconciler) handleRunningClusterImage(ctx context.Context,
|
|||||||
return r.updateClusterImageExportStatus(ctx, clusterImage)
|
return r.updateClusterImageExportStatus(ctx, clusterImage)
|
||||||
}
|
}
|
||||||
func (r *ClusterImageReconciler) cleanupJobAndPods(ctx context.Context, job *v1batch.Job) error {
|
func (r *ClusterImageReconciler) cleanupJobAndPods(ctx context.Context, job *v1batch.Job) error {
|
||||||
// Add a short delay to allow status updates to propagate
|
// Wait for job status to propagate before deletion
|
||||||
time.Sleep(2 * time.Second)
|
jobKey := types.NamespacedName{Name: job.Name, Namespace: job.Namespace}
|
||||||
|
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (done bool, err error) {
|
||||||
|
currentJob := &v1batch.Job{}
|
||||||
|
if err := r.Get(ctx, jobKey, currentJob); err != nil {
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return true, nil // Job already deleted
|
||||||
|
}
|
||||||
|
return false, nil // Retry on transient errors
|
||||||
|
}
|
||||||
|
// Job status has been updated, proceed with deletion
|
||||||
|
return currentJob.Status.Active == 0, nil
|
||||||
|
})
|
||||||
|
if err != nil && !errors.IsNotFound(err) && err != context.DeadlineExceeded {
|
||||||
|
return fmt.Errorf("failed to wait for job status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the job
|
// Delete the job
|
||||||
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {
|
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {
|
||||||
@@ -374,8 +400,8 @@ func (r *ClusterImageReconciler) createBackupJob(ctx context.Context, clusterIma
|
|||||||
Kind: clusterImage.Kind,
|
Kind: clusterImage.Kind,
|
||||||
Name: clusterImage.Name,
|
Name: clusterImage.Name,
|
||||||
UID: clusterImage.UID,
|
UID: clusterImage.UID,
|
||||||
BlockOwnerDeletion: pointer.Bool(true),
|
BlockOwnerDeletion: ptr.To(true),
|
||||||
Controller: pointer.Bool(true),
|
Controller: ptr.To(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package raczylocom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5" // #nosec G501 - MD5 used for non-cryptographic unique identifiers only
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/client-go/util/retry"
|
"k8s.io/client-go/util/retry"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/ptr"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
// ClusterImageExportReconciler reconciles a ClusterImageExport object
|
// ClusterImageExportReconciler reconciles a ClusterImageExport object
|
||||||
type ClusterImageExportReconciler struct {
|
type ClusterImageExportReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
podAnnotations map[string]string
|
podAnnotations map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +121,7 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
|
|
||||||
for _, image := range fullImagesList.Containers {
|
for _, image := range fullImagesList.Containers {
|
||||||
// Include creation timestamp in the hash to differentiate between exports with the same name
|
// Include creation timestamp in the hash to differentiate between exports with the same name
|
||||||
|
// #nosec G401 - MD5 used for non-cryptographic unique identifier generation, not security
|
||||||
nameHash := fmt.Sprintf("%x", md5.Sum([]byte(clusterImageExport.Name+image.Image+image.Tag+image.Sha+
|
nameHash := fmt.Sprintf("%x", md5.Sum([]byte(clusterImageExport.Name+image.Image+image.Tag+image.Sha+
|
||||||
clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"])))[:14]
|
clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"])))[:14]
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
Kind: clusterImageExport.Kind,
|
Kind: clusterImageExport.Kind,
|
||||||
Name: clusterImageExport.Name,
|
Name: clusterImageExport.Name,
|
||||||
UID: clusterImageExport.UID,
|
UID: clusterImageExport.UID,
|
||||||
Controller: pointer.Bool(true),
|
Controller: ptr.To(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -185,7 +186,7 @@ func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.R
|
|||||||
failedCount := 0
|
failedCount := 0
|
||||||
pendingCount := 0
|
pendingCount := 0
|
||||||
clusterImageList := &raczylocomv1.ClusterImageList{}
|
clusterImageList := &raczylocomv1.ClusterImageList{}
|
||||||
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace),
|
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace),
|
||||||
client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil {
|
client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil {
|
||||||
l.Error(err, "unable to list ClusterImages")
|
l.Error(err, "unable to list ClusterImages")
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
@@ -250,21 +251,6 @@ func (r *ClusterImageExportReconciler) updateStatusWithRetry(ctx context.Context
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClusterImageExportReconciler) checkAllClusterImagesCompleted(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) (bool, error) {
|
|
||||||
clusterImageList := &raczylocomv1.ClusterImageList{}
|
|
||||||
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace), client.MatchingFields{"spec.exportName": clusterImageExport.Name}); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ci := range clusterImageList.Items {
|
|
||||||
if ci.Status.Progress != shared.STATUS_SUCCESS && ci.Status.Progress != shared.STATUS_PRESENT {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupWithManager sets up the controller with the Manager.
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
|
||||||
func (r *ClusterImageExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *ClusterImageExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
@@ -409,7 +395,7 @@ func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, cluste
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up the cleanup job with retry limits and TTL
|
// Set up the cleanup job with retry limits and TTL
|
||||||
backoffLimit := int32(2) // 3 total attempts (initial + 2 retries)
|
backoffLimit := int32(2) // 3 total attempts (initial + 2 retries)
|
||||||
ttlSecondsAfterFinished := int32(300) // Delete job 5 minutes after completion
|
ttlSecondsAfterFinished := int32(300) // Delete job 5 minutes after completion
|
||||||
|
|
||||||
// Merge annotations from different sources
|
// Merge annotations from different sources
|
||||||
@@ -443,16 +429,16 @@ func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, cluste
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobParams := shared.JobParams{
|
jobParams := shared.JobParams{
|
||||||
Name: normalisedImageName,
|
Name: normalisedImageName,
|
||||||
Namespace: clusterImageExport.Namespace,
|
Namespace: clusterImageExport.Namespace,
|
||||||
Image: shared.BACKUP_JOB_IMAGE,
|
Image: shared.BACKUP_JOB_IMAGE,
|
||||||
Commands: defaultCommands,
|
Commands: defaultCommands,
|
||||||
Annotations: mergedAnnotations,
|
Annotations: mergedAnnotations,
|
||||||
ServiceAccount: "",
|
ServiceAccount: "",
|
||||||
ImagePullSecrets: clusterImageExport.Spec.ImagePullSecrets,
|
ImagePullSecrets: clusterImageExport.Spec.ImagePullSecrets,
|
||||||
BackoffLimit: &backoffLimit,
|
BackoffLimit: &backoffLimit,
|
||||||
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
|
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
|
||||||
EnvVars: envVars,
|
EnvVars: envVars,
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
cleanupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
var BACKUP_JOB_IMAGE string
|
var BACKUP_JOB_IMAGE string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
BACKUP_JOB_IMAGE = os.Getenv("WORKER_IMAGE")
|
BACKUP_JOB_IMAGE = os.Getenv("WORKER_IMAGE")
|
||||||
if BACKUP_JOB_IMAGE == "" {
|
if BACKUP_JOB_IMAGE == "" {
|
||||||
BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:latest" // fallback
|
BACKUP_JOB_IMAGE = "ghcr.io/lukaszraczylo/kubernetes-images-sync-worker:0.5.54" // fallback to known stable version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import (
|
|||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JobParams struct {
|
type JobParams struct {
|
||||||
Name string
|
Name string
|
||||||
Namespace string
|
Namespace string
|
||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
Image string
|
Image string
|
||||||
Commands []string
|
Commands []string
|
||||||
EnvVars []corev1.EnvVar
|
EnvVars []corev1.EnvVar
|
||||||
@@ -88,7 +88,7 @@ func CreateJob[T any](params JobParams, setupFunc func(T) []string) *batchv1.Job
|
|||||||
VolumeMounts: volumeMounts,
|
VolumeMounts: volumeMounts,
|
||||||
Env: params.EnvVars,
|
Env: params.EnvVars,
|
||||||
SecurityContext: &corev1.SecurityContext{
|
SecurityContext: &corev1.SecurityContext{
|
||||||
Privileged: pointer.Bool(true),
|
Privileged: ptr.To(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024.
|
|
||||||
|
|
||||||
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 e2e
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run e2e tests using the Ginkgo runner.
|
|
||||||
func TestE2E(t *testing.T) {
|
|
||||||
RegisterFailHandler(Fail)
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "Starting kubernetes-images-sync-operator suite\n")
|
|
||||||
RunSpecs(t, "e2e suite")
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024.
|
|
||||||
|
|
||||||
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 e2e
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
|
|
||||||
"github.com/lukaszraczylo/kubernetes-images-sync-operator/test/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const namespace = "kubernetes-images-sync-operator-system"
|
|
||||||
|
|
||||||
var _ = Describe("controller", Ordered, func() {
|
|
||||||
BeforeAll(func() {
|
|
||||||
By("installing prometheus operator")
|
|
||||||
Expect(utils.InstallPrometheusOperator()).To(Succeed())
|
|
||||||
|
|
||||||
By("installing the cert-manager")
|
|
||||||
Expect(utils.InstallCertManager()).To(Succeed())
|
|
||||||
|
|
||||||
By("creating manager namespace")
|
|
||||||
cmd := exec.Command("kubectl", "create", "ns", namespace)
|
|
||||||
_, _ = utils.Run(cmd)
|
|
||||||
})
|
|
||||||
|
|
||||||
AfterAll(func() {
|
|
||||||
By("uninstalling the Prometheus manager bundle")
|
|
||||||
utils.UninstallPrometheusOperator()
|
|
||||||
|
|
||||||
By("uninstalling the cert-manager bundle")
|
|
||||||
utils.UninstallCertManager()
|
|
||||||
|
|
||||||
By("removing manager namespace")
|
|
||||||
cmd := exec.Command("kubectl", "delete", "ns", namespace)
|
|
||||||
_, _ = utils.Run(cmd)
|
|
||||||
})
|
|
||||||
|
|
||||||
Context("Operator", func() {
|
|
||||||
It("should run successfully", func() {
|
|
||||||
var controllerPodName string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// projectimage stores the name of the image used in the example
|
|
||||||
var projectimage = "example.com/kubernetes-images-sync-operator:v0.0.1"
|
|
||||||
|
|
||||||
By("building the manager(Operator) image")
|
|
||||||
cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage))
|
|
||||||
_, err = utils.Run(cmd)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("loading the the manager(Operator) image on Kind")
|
|
||||||
err = utils.LoadImageToKindClusterWithName(projectimage)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("installing CRDs")
|
|
||||||
cmd = exec.Command("make", "install")
|
|
||||||
_, err = utils.Run(cmd)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("deploying the sa")
|
|
||||||
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage))
|
|
||||||
_, err = utils.Run(cmd)
|
|
||||||
ExpectWithOffset(1, err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
By("validating that the sa pod is running as expected")
|
|
||||||
verifyControllerUp := func() error {
|
|
||||||
// Get pod name
|
|
||||||
|
|
||||||
cmd = exec.Command("kubectl", "get",
|
|
||||||
"pods", "-l", "control-plane=sa",
|
|
||||||
"-o", "go-template={{ range .items }}"+
|
|
||||||
"{{ if not .metadata.deletionTimestamp }}"+
|
|
||||||
"{{ .metadata.name }}"+
|
|
||||||
"{{ \"\\n\" }}{{ end }}{{ end }}",
|
|
||||||
"-n", namespace,
|
|
||||||
)
|
|
||||||
|
|
||||||
podOutput, err := utils.Run(cmd)
|
|
||||||
ExpectWithOffset(2, err).NotTo(HaveOccurred())
|
|
||||||
podNames := utils.GetNonEmptyLines(string(podOutput))
|
|
||||||
if len(podNames) != 1 {
|
|
||||||
return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames))
|
|
||||||
}
|
|
||||||
controllerPodName = podNames[0]
|
|
||||||
ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("sa"))
|
|
||||||
|
|
||||||
// Validate pod status
|
|
||||||
cmd = exec.Command("kubectl", "get",
|
|
||||||
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
|
|
||||||
"-n", namespace,
|
|
||||||
)
|
|
||||||
status, err := utils.Run(cmd)
|
|
||||||
ExpectWithOffset(2, err).NotTo(HaveOccurred())
|
|
||||||
if string(status) != "Running" {
|
|
||||||
return fmt.Errorf("controller pod in %s status", status)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed())
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024.
|
|
||||||
|
|
||||||
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 utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2" //nolint:golint,revive
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
prometheusOperatorVersion = "v0.72.0"
|
|
||||||
prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" +
|
|
||||||
"releases/download/%s/bundle.yaml"
|
|
||||||
|
|
||||||
certmanagerVersion = "v1.14.4"
|
|
||||||
certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func warnError(err error) {
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics.
|
|
||||||
func InstallPrometheusOperator() error {
|
|
||||||
url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
|
|
||||||
cmd := exec.Command("kubectl", "create", "-f", url)
|
|
||||||
_, err := Run(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run executes the provided command within this context
|
|
||||||
func Run(cmd *exec.Cmd) ([]byte, error) {
|
|
||||||
dir, _ := GetProjectDir()
|
|
||||||
cmd.Dir = dir
|
|
||||||
|
|
||||||
if err := os.Chdir(cmd.Dir); err != nil {
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(), "GO111MODULE=on")
|
|
||||||
command := strings.Join(cmd.Args, " ")
|
|
||||||
_, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UninstallPrometheusOperator uninstalls the prometheus
|
|
||||||
func UninstallPrometheusOperator() {
|
|
||||||
url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
|
|
||||||
cmd := exec.Command("kubectl", "delete", "-f", url)
|
|
||||||
if _, err := Run(cmd); err != nil {
|
|
||||||
warnError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UninstallCertManager uninstalls the cert manager
|
|
||||||
func UninstallCertManager() {
|
|
||||||
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
|
||||||
cmd := exec.Command("kubectl", "delete", "-f", url)
|
|
||||||
if _, err := Run(cmd); err != nil {
|
|
||||||
warnError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallCertManager installs the cert manager bundle.
|
|
||||||
func InstallCertManager() error {
|
|
||||||
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
|
|
||||||
cmd := exec.Command("kubectl", "apply", "-f", url)
|
|
||||||
if _, err := Run(cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
|
|
||||||
// was re-installed after uninstalling on a cluster.
|
|
||||||
cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
|
|
||||||
"--for", "condition=Available",
|
|
||||||
"--namespace", "cert-manager",
|
|
||||||
"--timeout", "5m",
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err := Run(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
|
|
||||||
func LoadImageToKindClusterWithName(name string) error {
|
|
||||||
cluster := "kind"
|
|
||||||
if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
|
|
||||||
cluster = v
|
|
||||||
}
|
|
||||||
kindOptions := []string{"load", "docker-image", name, "--name", cluster}
|
|
||||||
cmd := exec.Command("kind", kindOptions...)
|
|
||||||
_, err := Run(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNonEmptyLines converts given command output string into individual objects
|
|
||||||
// according to line breakers, and ignores the empty elements in it.
|
|
||||||
func GetNonEmptyLines(output string) []string {
|
|
||||||
var res []string
|
|
||||||
elements := strings.Split(output, "\n")
|
|
||||||
for _, element := range elements {
|
|
||||||
if element != "" {
|
|
||||||
res = append(res, element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProjectDir will return the directory where the project is
|
|
||||||
func GetProjectDir() (string, error) {
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return wd, err
|
|
||||||
}
|
|
||||||
wd = strings.Replace(wd, "/test/e2e", "", -1)
|
|
||||||
return wd, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user