diff --git a/.github/workflows/dockerimage-push.yaml b/.github/workflows/dockerimage-push.yaml new file mode 100644 index 0000000..47b6e04 --- /dev/null +++ b/.github/workflows/dockerimage-push.yaml @@ -0,0 +1,31 @@ +# +# Copyright (c) 2020 Red Hat, Inc. +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +name: Next Dockerimage + +on: + push: + branches: [ main ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout registry-operator source code + uses: actions/checkout@v2 + - name: Docker Build & Push + uses: docker/build-push-action@v1.1.0 + with: + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + registry: quay.io + repository: devfile/registry-operator + dockerfile: Dockerfile + tags: next + tag_with_sha: true diff --git a/Dockerfile b/Dockerfile index 74eb9d7..c1b10a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN go mod download COPY main.go main.go COPY api/ api/ COPY controllers/ controllers/ +COPY pkg/ pkg/ # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go diff --git a/Makefile b/Makefile index 57279e0..5b3a6da 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,12 @@ ENVTEST_ASSETS_DIR = $(shell pwd)/testbin test: generate fmt vet manifests mkdir -p $(ENVTEST_ASSETS_DIR) test -f $(ENVTEST_ASSETS_DIR)/setup-envtest.sh || curl -sSLo $(ENVTEST_ASSETS_DIR)/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.6.3/hack/setup-envtest.sh - source $(ENVTEST_ASSETS_DIR)/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out + source $(ENVTEST_ASSETS_DIR)/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./pkg/... -coverprofile cover.out + +### test-integration: runs integration tests on the cluster set in context. +test-integration: generate fmt vet manifests + CGO_ENABLED=0 go test -v -c -o bin/devfileregistry-operator-integration ./tests/integration/cmd/devfileregistry_test.go + ./bin/devfileregistry-operator-integration # Build manager binary manager: generate fmt vet @@ -45,9 +50,10 @@ run: generate fmt vet manifests install: manifests kustomize $(KUSTOMIZE) build config/crd | kubectl apply -f - -# Uninstall CRDs from a cluster +# Uninstall operator and CRDs from a cluster uninstall: manifests kustomize - $(KUSTOMIZE) build config/crd | kubectl delete -f - + $(KUSTOMIZE) build config/default | kubectl delete -f - + #$(KUSTOMIZE) build config/crd | kubectl delete -f - # Deploy controller in the configured Kubernetes cluster in ~/.kube/config deploy: manifests kustomize diff --git a/README.md b/README.md index d2eeace..06414cf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ -# registry-operator -Operator for devfile registry +# Devfile Registry Operator + +Devfile Registry operator repository that contains the operator for the DevfileRegistry Custom Resource. + +## Issue Tracking + +Issue tracking repo: https://github.com/devfile/api with label area/registry + +## Running the controller in a cluster + +The controller can be deployed to a cluster provided you are logged in with cluster-admin credentials: + +```bash +export IMG=quay.io/devfile/registry-operator:next +make install && make deploy +``` + +## Development + +The repository contains a Makefile; building and deploying can be configured via the environment variables + +|variable|purpose|default value| +|---|---|---| +| `IMG` | Image used for controller | `quay.io/devfile/registry-operator:next` | + +Some of the rules supported by the makefile: + +|rule|purpose| +|---|---| +| docker-build | build registry operator docker image | +| docker-push | push registry operator docker image | +| deploy | deploy operator to cluster | +| install | create the devfile registry CRDs on the cluster | +| uninstall | remove the devfile registry operator and CRDs from the cluster | +| manifests | Generate manifests e.g. CRD, RBAC etc. | +| generate | Generate the API type definitions. Must be run after modifying the DevfileRegistry type. | +| test_integration | Run the integration tests for the operator. | + +To see all rules supported by the makefile, run `make help` + +## Testing + +To run integration tests for the operator, run `make test_integration`. + +By default, the tests will use the default image for the operator, `quay.io/devfile/registry-operator:next`. To use your own image, run: + +``` +export IMG= +make test_integration +``` + +### Run operator locally +It's possible to run an instance of the operator locally while communicating with a cluster. + +```bash +export NAMESPACE=devfileregistry-operator +make run ENABLE_WEBHOOKS=false +``` \ No newline at end of file diff --git a/api/v1alpha1/devfileregistry_types.go b/api/v1alpha1/devfileregistry_types.go index 05a4b3e..99169fe 100644 --- a/api/v1alpha1/devfileregistry_types.go +++ b/api/v1alpha1/devfileregistry_types.go @@ -17,26 +17,66 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// Important: Run "make" to regenerate code after modifying this file // DevfileRegistrySpec defines the desired state of DevfileRegistry type DevfileRegistrySpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + // Sets the container image containing devfile stacks to be deployed on the Devfile Registry + DevfileIndexImage string `json:"devfileIndexImage,omitempty"` - // Foo is an example field of DevfileRegistry. Edit DevfileRegistry_types.go to remove/update - Foo string `json:"foo,omitempty"` + // Overrides the container image used for the OCI registry. + // Recommended to leave blank and default to the image specified by the operator. + // +optional + OciRegistryImage string `json:"ociRegistryImage,omitempty"` + Storage DevfileRegistrySpecStorage `json:"storage,omitempty"` + TLS DevfileRegistrySpecTLS `json:"tls,omitempty"` + K8s DevfileRegistrySpecK8sOnly `json:"k8s,omitempty"` +} + +// DevfileRegistrySpecStorage defines the desired state of the storage for the DevfileRegistry +type DevfileRegistrySpecStorage struct { + // Instructs the operator to deploy the DevfileRegistry with persistent storage + // Enabled by default. Disabling is only recommended for development or test. + // +optional + Enabled *bool `json:"enabled,omitempty"` + + // Configures the size of the devfile registry's persistent volume, if enabled. + // Defaults to 1Gi. + // +optional + RegistryVolumeSize string `json:"ociRegistryImage,omitempty"` +} + +// DevfileRegistrySpecTLS defines the desired state for TLS in the DevfileRegistry +type DevfileRegistrySpecTLS struct { + // Instructs the operator to deploy the DevfileRegistry with TLS enabled. + // Enabled by default. Disabling is only recommended for development or test. + // +optional + Enabled *bool `json:"enabled,omitempty"` + + // Name of an optional, pre-existing TLS secret to use for TLS termination on ingress/route resources. + // +optional + SecretName string `json:"ociRegistryImage,omitempty"` +} + +// DevfileRegistrySpecK8sOnly defines the desired state of the kubernetes-only fields of the DevfileRegistry +type DevfileRegistrySpecK8sOnly struct { + // Ingress domain for a Kubernetes cluster. This MUST be explicitly specified on Kubernetes. There are no defaults + IngressDomain string `json:"ingressDomain,omitempty"` } // DevfileRegistryStatus defines the observed state of DevfileRegistry type DevfileRegistryStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // URL is the exposed URL for the Devfile Registry, and is set in the status after the registry has become available. + URL string `json:"url"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // DevfileRegistry is the Schema for the devfileregistries API +// +kubebuilder:resource:path=devfileregistries,shortName=devreg;dr +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="URL",type="string",JSONPath=".status.url",description="The URL for the Devfile Registry" type DevfileRegistry struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 47719cb..8380e53 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -24,7 +24,7 @@ func (in *DevfileRegistry) DeepCopyInto(out *DevfileRegistry) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -81,6 +81,9 @@ func (in *DevfileRegistryList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DevfileRegistrySpec) DeepCopyInto(out *DevfileRegistrySpec) { *out = *in + in.Storage.DeepCopyInto(&out.Storage) + in.TLS.DeepCopyInto(&out.TLS) + out.K8s = in.K8s } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevfileRegistrySpec. @@ -93,6 +96,61 @@ func (in *DevfileRegistrySpec) DeepCopy() *DevfileRegistrySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DevfileRegistrySpecK8sOnly) DeepCopyInto(out *DevfileRegistrySpecK8sOnly) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevfileRegistrySpecK8sOnly. +func (in *DevfileRegistrySpecK8sOnly) DeepCopy() *DevfileRegistrySpecK8sOnly { + if in == nil { + return nil + } + out := new(DevfileRegistrySpecK8sOnly) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DevfileRegistrySpecStorage) DeepCopyInto(out *DevfileRegistrySpecStorage) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevfileRegistrySpecStorage. +func (in *DevfileRegistrySpecStorage) DeepCopy() *DevfileRegistrySpecStorage { + if in == nil { + return nil + } + out := new(DevfileRegistrySpecStorage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DevfileRegistrySpecTLS) DeepCopyInto(out *DevfileRegistrySpecTLS) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevfileRegistrySpecTLS. +func (in *DevfileRegistrySpecTLS) DeepCopy() *DevfileRegistrySpecTLS { + if in == nil { + return nil + } + out := new(DevfileRegistrySpecTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DevfileRegistryStatus) DeepCopyInto(out *DevfileRegistryStatus) { *out = *in diff --git a/config/crd/bases/registry.devfile.io_devfileregistries.yaml b/config/crd/bases/registry.devfile.io_devfileregistries.yaml index 3e53c87..f4e3a8f 100644 --- a/config/crd/bases/registry.devfile.io_devfileregistries.yaml +++ b/config/crd/bases/registry.devfile.io_devfileregistries.yaml @@ -8,11 +8,19 @@ metadata: creationTimestamp: null name: devfileregistries.registry.devfile.io spec: + additionalPrinterColumns: + - JSONPath: .status.url + description: The URL for the Devfile Registry + name: URL + type: string group: registry.devfile.io names: kind: DevfileRegistry listKind: DevfileRegistryList plural: devfileregistries + shortNames: + - devreg + - dr singular: devfileregistry scope: Namespaced subresources: @@ -36,13 +44,62 @@ spec: spec: description: DevfileRegistrySpec defines the desired state of DevfileRegistry properties: - foo: - description: Foo is an example field of DevfileRegistry. Edit DevfileRegistry_types.go - to remove/update + devfileIndexImage: + description: Sets the container image containing devfile stacks to be + deployed on the Devfile Registry type: string + k8s: + description: DevfileRegistrySpecK8sOnly defines the desired state of + the kubernetes-only fields of the DevfileRegistry + properties: + ingressDomain: + description: Ingress domain for a Kubernetes cluster. This MUST + be explicitly specified on Kubernetes. There are no defaults + type: string + type: object + ociRegistryImage: + description: Overrides the container image used for the OCI registry. + Recommended to leave blank and default to the image specified by the + operator. + type: string + storage: + description: DevfileRegistrySpecStorage defines the desired state of + the storage for the DevfileRegistry + properties: + enabled: + description: Instructs the operator to deploy the DevfileRegistry + with persistent storage Enabled by default. Disabling is only + recommended for development or test. + type: boolean + ociRegistryImage: + description: Configures the size of the devfile registry's persistent + volume, if enabled. Defaults to 1Gi. + type: string + type: object + tls: + description: DevfileRegistrySpecTLS defines the desired state for TLS + in the DevfileRegistry + properties: + enabled: + description: Instructs the operator to deploy the DevfileRegistry + with TLS enabled. Enabled by default. Disabling is only recommended + for development or test. + type: boolean + ociRegistryImage: + description: Name of an optional, pre-existing TLS secret to use + for TLS termination on ingress/route resources. + type: string + type: object type: object status: description: DevfileRegistryStatus defines the observed state of DevfileRegistry + properties: + url: + description: URL is the exposed URL for the Devfile Registry, and is + set in the status after the registry has become available. + type: string + required: + - url type: object type: object version: v1alpha1 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b8..6a5141f 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: quay.io/devfile/registry-operator + newTag: next diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index b6c85a5..cea1706 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -12,15 +12,18 @@ metadata: namespace: system labels: control-plane: controller-manager + app.kubernetes.io/name: devfileregistry-operator spec: selector: matchLabels: control-plane: controller-manager + app.kubernetes.io/name: devfileregistry-operator replicas: 1 template: metadata: labels: control-plane: controller-manager + app.kubernetes.io/name: devfileregistry-operator spec: containers: - command: @@ -28,6 +31,7 @@ spec: args: - --enable-leader-election image: controller:latest + imagePullPolicy: Always name: manager resources: limits: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5f3230b..f266715 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -6,6 +6,50 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list +- apiGroups: + - extensions + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - registry.devfile.io resources: @@ -21,8 +65,22 @@ rules: - apiGroups: - registry.devfile.io resources: + - devfileregistries/finalizers - devfileregistries/status verbs: - get - patch - update +- apiGroups: + - route.openshift.io + resources: + - routes + - routes/custom-host + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/config/samples/registry_v1alpha1_devfileregistry.yaml b/config/samples/registry_v1alpha1_devfileregistry.yaml index 9a0944b..bf9c941 100644 --- a/config/samples/registry_v1alpha1_devfileregistry.yaml +++ b/config/samples/registry_v1alpha1_devfileregistry.yaml @@ -1,7 +1,6 @@ apiVersion: registry.devfile.io/v1alpha1 kind: DevfileRegistry metadata: - name: devfileregistry-sample + name: devfileregistry-tls spec: - # Add fields here - foo: bar + devfileIndexImage: quay.io/devfile/devfile-index:next \ No newline at end of file diff --git a/controllers/devfileregistry_controller.go b/controllers/devfileregistry_controller.go index 439ff7a..f4e63ee 100644 --- a/controllers/devfileregistry_controller.go +++ b/controllers/devfileregistry_controller.go @@ -13,13 +13,24 @@ package controllers import ( "context" + "time" "github.com/go-logr/logr" + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + "github.com/devfile/registry-operator/pkg/cluster" + "github.com/devfile/registry-operator/pkg/config" + "github.com/devfile/registry-operator/pkg/registry" + "github.com/devfile/registry-operator/pkg/util" ) // DevfileRegistryReconciler reconciles a DevfileRegistry object @@ -30,19 +41,139 @@ type DevfileRegistryReconciler struct { } // +kubebuilder:rbac:groups=registry.devfile.io,resources=devfileregistries,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=registry.devfile.io,resources=devfileregistries/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=registry.devfile.io,resources=devfileregistries/status;devfileregistries/finalizers,verbs=get;update;patch +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=services;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list; +// +kubebuilder:rbac:groups=extensions,resources=ingresses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=route.openshift.io,resources=routes;routes/custom-host,verbs=get;list;watch;create;update;patch;delete func (r *DevfileRegistryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("devfileregistry", req.NamespacedName) + ctx := context.Background() + log := r.Log.WithValues("devfileregistry", req.NamespacedName) - // your logic here + // Fetch the DevfileRegistry instance + devfileRegistry := ®istryv1alpha1.DevfileRegistry{} + err := r.Get(ctx, req.NamespacedName, devfileRegistry) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + log.Info("DevfileRegistry resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get DevfileRegistry") + return ctrl.Result{}, err + } + + // Generate labels for any subresources generated by the operator + labels := registry.LabelsForDevfileRegistry(devfileRegistry.Name) + + // Check if the service already exists, if not create a new one + result, err := r.ensureService(ctx, devfileRegistry, labels) + if result != nil { + return *result, err + } + + // If storage is enabled, create a persistent volume claim + if registry.IsStorageEnabled(devfileRegistry) { + // Check if the persistentvolumeclaim already exists, if not create a new one + result, err = r.ensurePVC(ctx, devfileRegistry, labels) + if result != nil { + return *result, err + } + } + + result, err = r.ensureDeployment(ctx, devfileRegistry, labels) + if result != nil { + return *result, err + } + + // Check to see if there's an old PVC that needs to be deleted + // Has to happen AFTER the deployment has been updated. + err = r.deleteOldPVCIfNeeded(ctx, devfileRegistry) + if err != nil { + return ctrl.Result{}, err + } + + // Create/update the ingress/route for the devfile registry + hostname := devfileRegistry.Spec.K8s.IngressDomain + if config.ControllerCfg.IsOpenShift() && hostname == "" { + // Check if the route exposing the devfile index exists + result, err = r.ensureRoute(ctx, devfileRegistry, labels) + if result != nil { + return *result, err + } + + // Get the hostname of the generated devfile route + devfilesRoute := &routev1.Route{} + err = r.Get(ctx, types.NamespacedName{Name: devfileRegistry.Name + "-devfiles", Namespace: devfileRegistry.Namespace}, devfilesRoute) + if err != nil { + // Log an error, but requeue, as the controller's cached kube client likely hasn't registered the new route yet. + // See https://github.com/operator-framework/operator-sdk/issues/4013#issuecomment-707267616 for an explanation on why we requeue rather than error out here + log.Error(err, "Failed to get Route") + return ctrl.Result{Requeue: true}, nil + } + hostname = devfilesRoute.Spec.Host + } else { + // Create/update the ingress for the devfile registry + hostname = registry.GetDevfileRegistryIngress(devfileRegistry) + result, err = r.ensureIngress(ctx, devfileRegistry, hostname, labels) + if result != nil { + return *result, err + } + } + + var devfileRegistryServer string + if registry.IsTLSEnabled(devfileRegistry) { + devfileRegistryServer = "https://" + hostname + } else { + devfileRegistryServer = "http://" + hostname + } + + if devfileRegistry.Status.URL != devfileRegistryServer { + // Check to see if the registry is active, and if so, update the status to reflect the URL + + err = util.WaitForServer(devfileRegistryServer, 30*time.Second) + if err != nil { + log.Error(err, "Devfile registry server failed to start after 30 seconds, requeing...") + return ctrl.Result{Requeue: true}, err + } + + // Update the status + devfileRegistry.Status.URL = devfileRegistryServer + err := r.Status().Update(ctx, devfileRegistry) + if err != nil { + log.Error(err, "Failed to update DevfileRegistry status") + return ctrl.Result{Requeue: true}, err + } + } return ctrl.Result{}, nil } func (r *DevfileRegistryReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). + // Check if we're running on OpenShift + isOS, err := cluster.IsOpenShift() + if err != nil { + return err + } + config.ControllerCfg.SetIsOpenShift(isOS) + + builder := ctrl.NewControllerManagedBy(mgr). For(®istryv1alpha1.DevfileRegistry{}). - Complete(r) + Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). + Owns(&corev1.PersistentVolumeClaim{}). + Owns(&v1beta1.Ingress{}) + + // If on OpenShift, mark routes as owned by the controller + if config.ControllerCfg.IsOpenShift() { + builder.Owns(&routev1.Route{}) + } + + return builder.Complete(r) + } diff --git a/controllers/ensure.go b/controllers/ensure.go new file mode 100644 index 0000000..469bad5 --- /dev/null +++ b/controllers/ensure.go @@ -0,0 +1,154 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package controllers + +import ( + "context" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + "github.com/devfile/registry-operator/pkg/registry" + routev1 "github.com/openshift/api/route/v1" + "github.com/prometheus/common/log" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ensureService ensures that a service for the devfile registry exists on the cluster and is up to date with the custom resource +func (r *DevfileRegistryReconciler) ensureService(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, labels map[string]string) (*reconcile.Result, error) { + // Check if the service already exists, if not create a new one + svc := &corev1.Service{} + err := r.Get(ctx, types.NamespacedName{Name: registry.ServiceName(cr.Name), Namespace: cr.Namespace}, svc) + if err != nil && errors.IsNotFound(err) { + // Define a new service + svc := registry.GenerateService(cr, r.Scheme, labels) + log.Info("Creating a new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name) + err = r.Create(ctx, svc) + if err != nil { + log.Error(err, "Failed to create new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name) + return &ctrl.Result{}, err + } + return nil, nil + } else if err != nil { + log.Error(err, "Failed to get Service") + return &ctrl.Result{}, err + } + + return nil, nil +} + +// ensureDeployment ensures that a devfile registry deployment exists on the cluster and is up to date with the custom resource +func (r *DevfileRegistryReconciler) ensureDeployment(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, labels map[string]string) (*reconcile.Result, error) { + dep := &appsv1.Deployment{} + err := r.Get(ctx, types.NamespacedName{Name: registry.DeploymentName(cr.Name), Namespace: cr.Namespace}, dep) + if err != nil && errors.IsNotFound(err) { + // Generate a new Deployment template and create it on the cluster + dep = registry.GenerateDeployment(cr, r.Scheme, labels) + + log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + err = r.Create(ctx, dep) + if err != nil { + log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return &ctrl.Result{}, err + } + return nil, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + return &ctrl.Result{}, err + } + + err = r.updateDeployment(ctx, cr, dep) + if err != nil { + log.Error(err, "Failed to update Deployment") + return &ctrl.Result{}, err + } + return nil, nil +} + +func (r *DevfileRegistryReconciler) ensurePVC(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, labels map[string]string) (*reconcile.Result, error) { + // Check if the persistentvolumeclaim already exists, if not create a new one + pvc := &corev1.PersistentVolumeClaim{} + err := r.Get(ctx, types.NamespacedName{Name: registry.PVCName(cr.Name), Namespace: cr.Namespace}, pvc) + if err != nil && errors.IsNotFound(err) { + // Define a new PVC + pvc := registry.GeneratePVC(cr, r.Scheme, labels) + log.Info("Creating a new PersistentVolumeClaim", "PersistentVolumeClaim.Namespace", pvc.Namespace, "PersistentVolumeClaim.Name", pvc.Name) + err = r.Create(ctx, pvc) + if err != nil { + log.Error(err, "Failed to create new PersistentVolumeClaim", "PersistentVolumeClaim.Namespace", pvc.Namespace, "PersistentVolumeClaim.Name", pvc.Name) + return &ctrl.Result{}, err + } + return nil, nil + } else if err != nil { + log.Error(err, "Failed to get PersistentVolumeClaim") + return &ctrl.Result{}, err + } + + return nil, nil +} + +func (r *DevfileRegistryReconciler) ensureRoute(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, labels map[string]string) (*reconcile.Result, error) { + route := &routev1.Route{} + err := r.Get(ctx, types.NamespacedName{Name: registry.DevfilesRouteName(cr.Name), Namespace: cr.Namespace}, route) + if err != nil && errors.IsNotFound(err) { + // Define a new route exposing the devfile registry index + route := registry.GenerateRoute(cr, r.Scheme, labels) + log.Info("Creating a new Route", "Route.Namespace", route.Namespace, "Route.Name", route.Name) + err = r.Create(ctx, route) + if err != nil { + log.Error(err, "Failed to create new Route", "Route.Namespace", route.Namespace, "Route.Name", route.Name) + return &ctrl.Result{}, err + } + return nil, nil + } else if err != nil { + log.Error(err, "Failed to get Route") + return &ctrl.Result{}, err + } + + err = r.updateRoute(ctx, cr, route) + if err != nil { + log.Error(err, "Failed to update Route") + return &ctrl.Result{}, err + } + + return nil, nil +} + +func (r *DevfileRegistryReconciler) ensureIngress(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, hostname string, labels map[string]string) (*reconcile.Result, error) { + ingress := &v1beta1.Ingress{} + err := r.Get(ctx, types.NamespacedName{Name: registry.IngressName(cr.Name), Namespace: cr.Namespace}, ingress) + if err != nil && errors.IsNotFound(err) { + // Define a new ingress exposing the devfile index and oci registry + ingress = registry.GenerateIngress(cr, hostname, r.Scheme, labels) + log.Info("Creating a new Ingress", "Ingress.Namespace", ingress.Namespace, "Ingress.Name", ingress.Name) + err = r.Create(ctx, ingress) + if err != nil { + log.Error(err, "Failed to create new Ingress", "Ingress.Namespace", ingress.Namespace, "Ingress.Name", ingress.Name) + return &ctrl.Result{}, err + } + return nil, nil + } else if err != nil { + log.Error(err, "Failed to get Ingress") + return &ctrl.Result{}, err + } + + err = r.updateIngress(ctx, cr, hostname, ingress) + if err != nil { + log.Error(err, "Failed to update Ingress") + return &ctrl.Result{}, err + } + return nil, nil +} diff --git a/controllers/update.go b/controllers/update.go new file mode 100644 index 0000000..9fed2b8 --- /dev/null +++ b/controllers/update.go @@ -0,0 +1,156 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package controllers + +import ( + "context" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + "github.com/devfile/registry-operator/pkg/registry" + routev1 "github.com/openshift/api/route/v1" + "github.com/prometheus/common/log" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// updateDeployment ensures that a devfile registry deployment exists on the cluster and is up to date with the custom resource +func (r *DevfileRegistryReconciler) updateDeployment(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, dep *appsv1.Deployment) error { + // Check to see if the existing devfile registry deployment needs to be updated + needsUpdating := false + if dep.Spec.Template.Spec.Containers[0].Image != cr.Spec.DevfileIndexImage { + dep.Spec.Template.Spec.Containers[0].Image = cr.Spec.DevfileIndexImage + needsUpdating = true + } + ociImage := registry.GetOCIRegistryImage(cr) + if dep.Spec.Template.Spec.Containers[1].Image != ociImage { + dep.Spec.Template.Spec.Containers[1].Image = ociImage + needsUpdating = true + } + + if registry.IsStorageEnabled(cr) { + if dep.Spec.Template.Spec.Volumes[0].PersistentVolumeClaim == nil { + dep.Spec.Template.Spec.Volumes[0].VolumeSource = registry.GetDevfileRegistryVolumeSource(cr) + needsUpdating = true + } + } else { + if dep.Spec.Template.Spec.Volumes[0].PersistentVolumeClaim != nil { + dep.Spec.Template.Spec.Volumes[0].VolumeSource = registry.GetDevfileRegistryVolumeSource(cr) + needsUpdating = true + } + } + if needsUpdating { + log.Info("Updating the DevfileRegistry deployment") + return r.Update(ctx, dep) + } + return nil +} + +// updateRoute checks to see if any of the fields in an existing devfile index route needs updating +func (r *DevfileRegistryReconciler) updateRoute(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, route *routev1.Route) error { + needsUpdating := false + + // Check to see if TLS fields were updated + if registry.IsTLSEnabled(cr) { + if route.Spec.TLS == nil { + route.Spec.TLS = &routev1.TLSConfig{Termination: routev1.TLSTerminationEdge} + needsUpdating = true + } + } else { + if route.Spec.TLS != nil { + route.Spec.TLS = nil + needsUpdating = true + } + } + + if needsUpdating { + return r.Update(ctx, route) + } + return nil +} + +// updateIngress checks to see if any of the fields in an existing ingress resouorce need to be updated +func (r *DevfileRegistryReconciler) updateIngress(ctx context.Context, cr *registryv1alpha1.DevfileRegistry, hostname string, ingress *v1beta1.Ingress) error { + needsUpdating := false + // Check to see if TLS fields were updated + if registry.IsTLSEnabled(cr) { + if len(ingress.Spec.TLS) == 0 { + // TLS was toggled on, so enable it in the ingress spec + ingress.Spec.TLS = []v1beta1.IngressTLS{ + { + Hosts: []string{hostname}, + SecretName: cr.Spec.TLS.SecretName, + }, + } + needsUpdating = true + } + if ingress.Spec.TLS[0].SecretName != cr.Spec.TLS.SecretName { + // TLS secret name was updated, so update it in the ingress spec + ingress.Spec.TLS[0].SecretName = cr.Spec.TLS.SecretName + needsUpdating = true + } + } else { + if len(ingress.Spec.TLS) > 0 { + // TLS was disabled, so disable it in the ingress spec + ingress.Spec.TLS = []v1beta1.IngressTLS{} + needsUpdating = true + } + } + + // Check to see if the ingress domain was updated + if ingress.Spec.Rules[0].Host != hostname { + ingress.Spec.Rules[0].Host = hostname + + // If TLS is enabled, need to update the hostname there too + if registry.IsTLSEnabled(cr) { + ingress.Spec.TLS[0].Hosts = []string{hostname} + } + needsUpdating = true + } + + if needsUpdating { + return r.Update(ctx, ingress, &client.UpdateOptions{}) + } + + return nil +} + +// deletePVCIfNeeded deletes the PVC for the devfile registry if one exists and if persistent storage was disabled +func (r *DevfileRegistryReconciler) deleteOldPVCIfNeeded(ctx context.Context, cr *registryv1alpha1.DevfileRegistry) error { + // Check to see if a PVC exists, if so, need to clean it up because storage was disabled + if !registry.IsStorageEnabled(cr) { + pvc := &corev1.PersistentVolumeClaim{} + err := r.Get(ctx, types.NamespacedName{Name: registry.PVCName(cr.Name), Namespace: cr.Namespace}, pvc) + if err != nil { + if errors.IsNotFound(err) { + // PVC not found, so there's no old PVC to delete. Just return nil, nothing to do. + return nil + } else { + // Some other error occurred when listing PVCs, so log and return an error + log.Error(err, "Error listing PersistentVolumeClaims") + return err + } + } else { + // PVC found despite storage being disable, so delete it + log.Info(err, "Old PersistentVolumeClaim", pvc.Name, "found. Deleting it as storage has been disabled.") + err = r.Delete(ctx, pvc) + if err != nil { + log.Error(err, "Error deleting PersistentVolumeClaim", pvc.Name) + return err + } + } + } + return nil +} diff --git a/go.mod b/go.mod index 23bb6e1..3a1fff2 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,10 @@ require ( github.com/go-logr/logr v0.1.0 github.com/onsi/ginkgo v1.12.1 github.com/onsi/gomega v1.10.1 + github.com/openshift/api v0.0.0-20200205133042-34f0ec8dab87 + github.com/prometheus/common v0.4.1 + gopkg.in/yaml.v2 v2.3.0 + k8s.io/api v0.18.6 k8s.io/apimachinery v0.18.6 k8s.io/client-go v0.18.6 sigs.k8s.io/controller-runtime v0.6.2 diff --git a/go.sum b/go.sum index d697bf9..540ba83 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -18,7 +19,9 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -44,6 +47,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -55,6 +59,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -123,6 +128,7 @@ github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85n github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -149,6 +155,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -180,6 +187,7 @@ github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -190,6 +198,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -215,6 +224,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh 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 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -227,6 +237,7 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -235,12 +246,15 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/openshift/api v0.0.0-20200205133042-34f0ec8dab87 h1:L/fZlWB7DdYCd09r9LvBa44xRH42Dx80ybxfN1h5C8Y= +github.com/openshift/api v0.0.0-20200205133042-34f0ec8dab87/go.mod h1:fT6U/JfG8uZzemTRwZA2kBDJP5nWz7v05UHnty/D+pk= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= @@ -258,10 +272,12 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -279,6 +295,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= @@ -310,13 +327,19 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -334,6 +357,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -355,6 +379,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -383,18 +408,25 @@ golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200115044656-831fdb1e1868/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -415,6 +447,7 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -440,18 +473,22 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.17.1/go.mod h1:zxiAc5y8Ngn4fmhWUtSxuUlkfz1ixT7j9wESokELzOg= k8s.io/api v0.18.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE= k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI= k8s.io/apiextensions-apiserver v0.18.6 h1:vDlk7cyFsDyfwn2rNAO2DbmUbvXy5yT5GE3rrqOzaMo= k8s.io/apiextensions-apiserver v0.18.6/go.mod h1:lv89S7fUysXjLZO7ke783xOwVTm6lKizADfvUM/SS/M= +k8s.io/apimachinery v0.17.1/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag= k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= k8s.io/client-go v0.18.6 h1:I+oWqJbibLSGsZj8Xs8F0aWVXJVIoUHWaaJV3kUN/Zw= k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q= +k8s.io/code-generator v0.17.1/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= k8s.io/code-generator v0.18.6/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= k8s.io/component-base v0.18.6/go.mod h1:knSVsibPR5K6EW2XOjEHik6sdU5nCvKMrzMt2D4In14= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -459,14 +496,22 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY= k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 h1:v8ud2Up6QK1lNOKFgiIVrZdMg7MpmSnvtrOieolJKoE= k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= sigs.k8s.io/controller-runtime v0.6.2 h1:jkAnfdTYBpFwlmBn3pS5HFO06SfxvnTZ1p5PeEF/zAA= sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= diff --git a/main.go b/main.go index fc3b0a8..8e21f28 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,8 @@ import ( registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" "github.com/devfile/registry-operator/controllers" + + routev1 "github.com/openshift/api/route/v1" // +kubebuilder:scaffold:imports ) @@ -34,7 +36,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - + utilruntime.Must(routev1.AddToScheme(scheme)) utilruntime.Must(registryv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } diff --git a/pkg/cluster/info.go b/pkg/cluster/info.go new file mode 100644 index 0000000..a6d5dd2 --- /dev/null +++ b/pkg/cluster/info.go @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package cluster + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +// Borrowed from https://github.com/devfile/devworkspace-operator/blob/master/internal/cluster/info.go + +// IsOpenShift returns true if the operator is running on an OpenShift cluster +func IsOpenShift() (bool, error) { + kubeCfg, err := config.GetConfig() + if err != nil { + return false, err + } + discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg) + if err != nil { + return false, err + } + apiList, err := discoveryClient.ServerGroups() + if err != nil { + return false, err + } + if findAPIGroup(apiList.Groups, "route.openshift.io") == nil { + return false, nil + } else { + return true, nil + } +} + +func findAPIGroup(source []metav1.APIGroup, apiName string) *metav1.APIGroup { + for i := 0; i < len(source); i++ { + if source[i].Name == apiName { + return &source[i] + } + } + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..00c9200 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,27 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package config + +// ControllerCfg logic borrowed from https://github.com/devfile/devworkspace-operator/blob/master/pkg/config/config.go +var ControllerCfg ControllerConfig + +type ControllerConfig struct { + isOpenShift bool +} + +func (c *ControllerConfig) IsOpenShift() bool { + return c.isOpenShift +} + +func (c *ControllerConfig) SetIsOpenShift(isOpenShift bool) { + c.isOpenShift = isOpenShift +} diff --git a/pkg/registry/defaults.go b/pkg/registry/defaults.go new file mode 100644 index 0000000..00a8d0e --- /dev/null +++ b/pkg/registry/defaults.go @@ -0,0 +1,78 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +const ( + // Default image:tags + DefaultDevfileIndexImage = "quay.io/devfile/devfile-index:next" + DefaultOCIRegistryImage = "registry:2.7.1" + + // Defaults/constants for devfile registry storages + DefaultDevfileRegistryVolumeSize = "1Gi" + DevfileRegistryVolumeEnabled = true + DevfileRegistryVolumeName = "devfile-registry-storage" + + DevfileRegistryTLSEnabled = true + + // Defaults/constants for devfile registry services + DevfileIndexPortName = "devfile-registry-metadata" + DevfileIndexPort = 8080 +) + +func GetOCIRegistryImage(cr *registryv1alpha1.DevfileRegistry) string { + if cr.Spec.OciRegistryImage != "" { + return cr.Spec.OciRegistryImage + } + return DefaultOCIRegistryImage +} + +func getDevfileRegistryVolumeSize(cr *registryv1alpha1.DevfileRegistry) string { + if cr.Spec.Storage.RegistryVolumeSize != "" { + return cr.Spec.Storage.RegistryVolumeSize + } + return DefaultDevfileRegistryVolumeSize +} + +func GetDevfileRegistryVolumeSource(cr *registryv1alpha1.DevfileRegistry) corev1.VolumeSource { + if IsStorageEnabled(cr) { + return corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: PVCName(cr.Name), + }, + } + } + // If persistence is not enabled, return an empty dir volume source + return corev1.VolumeSource{} +} + +// IsStorageEnabled returns true if storage.enabled is set in the DevfileRegistry CR +// If it's not set, it returns true by default. +func IsStorageEnabled(cr *registryv1alpha1.DevfileRegistry) bool { + if cr.Spec.Storage.Enabled != nil { + return *cr.Spec.Storage.Enabled + } + return DevfileRegistryVolumeEnabled +} + +// IsTLSEnabled returns true if tls.enabled is set in the DevfileRegistry CR +// If it's not set, it returns true by default. +func IsTLSEnabled(cr *registryv1alpha1.DevfileRegistry) bool { + if cr.Spec.TLS.Enabled != nil { + return *cr.Spec.TLS.Enabled + } + return DevfileRegistryTLSEnabled +} diff --git a/pkg/registry/defaults_test.go b/pkg/registry/defaults_test.go new file mode 100644 index 0000000..1a182c0 --- /dev/null +++ b/pkg/registry/defaults_test.go @@ -0,0 +1,225 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + "reflect" + "testing" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIsTLSEnabled(t *testing.T) { + tlsEnabled := true + tlsDisabled := false + + tests := []struct { + name string + cr registryv1alpha1.DevfileRegistry + want bool + }{ + { + name: "Case 1: TLS enabled in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{ + TLS: registryv1alpha1.DevfileRegistrySpecTLS{ + Enabled: &tlsEnabled, + }, + }, + }, + want: true, + }, + { + name: "Case 2: TLS disabled in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{ + TLS: registryv1alpha1.DevfileRegistrySpecTLS{ + Enabled: &tlsDisabled, + }, + }, + }, + want: false, + }, + { + name: "Case 3: TLS not set, default set to true", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tlsSetting := IsTLSEnabled(&tt.cr) + if tlsSetting != tt.want { + t.Errorf("TestIsTLSEnabled error: tls value mismatch, expected: %v got: %v", tt.want, tlsSetting) + } + }) + } + +} + +func TestIsStorageEnabled(t *testing.T) { + storageEnabled := true + storageDisabled := false + + tests := []struct { + name string + cr registryv1alpha1.DevfileRegistry + want bool + }{ + { + name: "Case 1: Storage enabled in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{ + Storage: registryv1alpha1.DevfileRegistrySpecStorage{ + Enabled: &storageEnabled, + }, + }, + }, + want: true, + }, + { + name: "Case 2: Storage disabled in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{ + Storage: registryv1alpha1.DevfileRegistrySpecStorage{ + Enabled: &storageDisabled, + }, + }, + }, + want: false, + }, + { + name: "Case 3: Storage not set, default set to true", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tlsSetting := IsStorageEnabled(&tt.cr) + if tlsSetting != tt.want { + t.Errorf("TestIsStorageEnabled error: storage value mismatch, expected: %v got: %v", tt.want, tlsSetting) + } + }) + } + +} + +func TestGetDevfileRegistryVolumeSource(t *testing.T) { + storageEnabled := true + storageDisabled := false + crName := "devfileregistry-test" + + tests := []struct { + name string + cr registryv1alpha1.DevfileRegistry + want corev1.VolumeSource + }{ + { + name: "Case 1: Storage enabled in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + }, + Spec: registryv1alpha1.DevfileRegistrySpec{ + Storage: registryv1alpha1.DevfileRegistrySpecStorage{ + Enabled: &storageEnabled, + }, + }, + }, + want: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: PVCName(crName), + }, + }, + }, + { + name: "Case 2: Storage disabled in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + }, + Spec: registryv1alpha1.DevfileRegistrySpec{ + Storage: registryv1alpha1.DevfileRegistrySpecStorage{ + Enabled: &storageDisabled, + }, + }, + }, + want: corev1.VolumeSource{}, + }, + { + name: "Case 3: Storage not set, default set to true", + cr: registryv1alpha1.DevfileRegistry{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + }, + Spec: registryv1alpha1.DevfileRegistrySpec{}, + }, + want: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: PVCName(crName), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tlsSetting := GetDevfileRegistryVolumeSource(&tt.cr) + if !reflect.DeepEqual(tlsSetting, tt.want) { + t.Errorf("TestGetDevfileRegistryVolumeSource error: storage source mismatch, expected: %v got: %v", tt.want, tlsSetting) + } + }) + } + +} + +func TestGetDevfileRegistryVolumeSize(t *testing.T) { + tests := []struct { + name string + cr registryv1alpha1.DevfileRegistry + want string + }{ + { + name: "Case 1: Volume size set in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{ + Storage: registryv1alpha1.DevfileRegistrySpecStorage{ + RegistryVolumeSize: "5Gi", + }, + }, + }, + want: "5Gi", + }, + { + name: "Case 2: Volume size not set in DevfileRegistry CR", + cr: registryv1alpha1.DevfileRegistry{ + Spec: registryv1alpha1.DevfileRegistrySpec{}, + }, + want: DefaultDevfileRegistryVolumeSize, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + volSize := getDevfileRegistryVolumeSize(&tt.cr) + if volSize != tt.want { + t.Errorf("TestGetDevfileRegistryVolumeSize error: storage size mismatch, expected: %v got: %v", tt.want, volSize) + } + }) + } + +} diff --git a/pkg/registry/deployment.go b/pkg/registry/deployment.go new file mode 100644 index 0000000..5b61cc0 --- /dev/null +++ b/pkg/registry/deployment.go @@ -0,0 +1,109 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" +) + +func GenerateDeployment(cr *registryv1alpha1.DevfileRegistry, scheme *runtime.Scheme, labels map[string]string) *appsv1.Deployment { + replicas := int32(1) + + dep := &appsv1.Deployment{ + ObjectMeta: generateObjectMeta(cr.Name, cr.Namespace, labels), + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: cr.Spec.DevfileIndexImage, + Name: "devfile-registry-bootstrap", + Ports: []corev1.ContainerPort{{ + ContainerPort: DevfileIndexPort, + }}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("250m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + }, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.FromInt(DevfileIndexPort), + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.FromInt(DevfileIndexPort), + }, + }, + }, + }, + { + Image: GetOCIRegistryImage(cr), + Name: "oci-registry", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: DevfileRegistryVolumeName, + MountPath: "/var/lib/registry", + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: DevfileRegistryVolumeName, + VolumeSource: GetDevfileRegistryVolumeSource(cr), + }, + }, + }, + }, + }, + } + // Set Memcached instance as the owner and controller + ctrl.SetControllerReference(cr, dep, scheme) + return dep +} diff --git a/pkg/registry/ingress.go b/pkg/registry/ingress.go new file mode 100644 index 0000000..396f92c --- /dev/null +++ b/pkg/registry/ingress.go @@ -0,0 +1,63 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" +) + +func GenerateIngress(cr *registryv1alpha1.DevfileRegistry, host string, scheme *runtime.Scheme, labels map[string]string) *v1beta1.Ingress { + ingress := &v1beta1.Ingress{ + ObjectMeta: generateObjectMeta(IngressName(cr.Name), cr.Namespace, labels), + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: host, + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/", + Backend: v1beta1.IngressBackend{ + ServiceName: ServiceName(cr.Name), + ServicePort: intstr.FromInt(int(DevfileIndexPort)), + }, + }, + }, + }, + }, + }, + }, + }, + } + + if IsTLSEnabled(cr) && cr.Spec.TLS.SecretName != "" { + ingress.Spec.TLS = []v1beta1.IngressTLS{ + { + Hosts: []string{host}, + SecretName: cr.Spec.TLS.SecretName, + }, + } + } + + // Set DevfileRegistry instance as the owner and controller + ctrl.SetControllerReference(cr, ingress, scheme) + return ingress +} + +func GetDevfileRegistryIngress(cr *registryv1alpha1.DevfileRegistry) string { + return cr.Name + "." + cr.Spec.K8s.IngressDomain +} diff --git a/pkg/registry/naming.go b/pkg/registry/naming.go new file mode 100644 index 0000000..580f264 --- /dev/null +++ b/pkg/registry/naming.go @@ -0,0 +1,46 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +// DeploymentName returns the name of the deployment object associated with the DevfileRegistry CR +// Just returns the CR name right now, but extracting to a function to avoid relying on that assumption in the future +func DeploymentName(devfileRegistryName string) string { + return devfileRegistryName +} + +// ServiceName returns the name of the service object associated with the DevfileRegistry CR +// Just returns the CR name right now, but extracting to a function to avoid relying on that assumption in the future +func ServiceName(devfileRegistryName string) string { + return devfileRegistryName +} + +// PVCName returns the name of the PVC object associated with the DevfileRegistry CR +// Just returns the CR name right now, but extracting to a function to avoid relying on that assumption in the future +func PVCName(devfileRegistryName string) string { + return devfileRegistryName +} + +// IngressName returns the name of the Ingress object associated with the DevfileRegistry CR +// Just returns the CR name right now, but extracting to a function to avoid relying on that assumption in the future +func IngressName(devfileRegistryName string) string { + return devfileRegistryName +} + +// DevfilesRouteName returns the name of the route object associated with the devfile index route +func DevfilesRouteName(devfileRegistryName string) string { + return devfileRegistryName + "-devfiles" +} + +// OCIRouteName returns the name of the route object associated with the OCI registry route +func OCIRouteName(devfileRegistryName string) string { + return devfileRegistryName + "-oci" +} diff --git a/pkg/registry/route.go b/pkg/registry/route.go new file mode 100644 index 0000000..7dd905c --- /dev/null +++ b/pkg/registry/route.go @@ -0,0 +1,49 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + routev1 "github.com/openshift/api/route/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" +) + +// GenerateRoute returns a route exposing the devfile registry index +func GenerateRoute(cr *registryv1alpha1.DevfileRegistry, scheme *runtime.Scheme, labels map[string]string) *routev1.Route { + weight := int32(100) + + route := &routev1.Route{ + ObjectMeta: generateObjectMeta(DevfilesRouteName(cr.Name), cr.Namespace, labels), + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: ServiceName(cr.Name), + Weight: &weight, + }, + Port: &routev1.RoutePort{ + TargetPort: intstr.FromString(DevfileIndexPortName), + }, + Path: "/", + }, + } + + if IsTLSEnabled(cr) { + route.Spec.TLS = &routev1.TLSConfig{Termination: routev1.TLSTerminationEdge} + } + + // Set DevfileRegistry instance as the owner and controller + ctrl.SetControllerReference(cr, route, scheme) + return route +} diff --git a/pkg/registry/service.go b/pkg/registry/service.go new file mode 100644 index 0000000..ab6576b --- /dev/null +++ b/pkg/registry/service.go @@ -0,0 +1,40 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" +) + +// GenerateDevfileRegistryService returns a devfileregistry Service object +func GenerateService(cr *registryv1alpha1.DevfileRegistry, scheme *runtime.Scheme, labels map[string]string) *corev1.Service { + svc := &corev1.Service{ + ObjectMeta: generateObjectMeta(ServiceName(cr.Name), cr.Namespace, labels), + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: DevfileIndexPortName, + Port: DevfileIndexPort, + }, + }, + Selector: labels, + }, + } + + // Set DevfileRegistry instance as the owner and controller + ctrl.SetControllerReference(cr, svc, scheme) + return svc +} diff --git a/pkg/registry/util.go b/pkg/registry/util.go new file mode 100644 index 0000000..8c79a95 --- /dev/null +++ b/pkg/registry/util.go @@ -0,0 +1,28 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +func generateObjectMeta(name string, namespace string, labels map[string]string) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + } +} + +// LabelsForDevfileRegistry returns the labels for selecting the resources +// belonging to the given devfileregistry CR name. +func LabelsForDevfileRegistry(name string) map[string]string { + return map[string]string{"app": "devfileregistry", "devfileregistry_cr": name} +} diff --git a/pkg/registry/volume.go b/pkg/registry/volume.go new file mode 100644 index 0000000..375b877 --- /dev/null +++ b/pkg/registry/volume.go @@ -0,0 +1,40 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation + +package registry + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" +) + +// GenerateDevfileRegistryPVC returns a PVC for providing storage on the OCI registry container +func GeneratePVC(cr *registryv1alpha1.DevfileRegistry, scheme *runtime.Scheme, labels map[string]string) *corev1.PersistentVolumeClaim { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: generateObjectMeta(cr.Name, cr.Namespace, labels), + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(getDevfileRegistryVolumeSize(cr)), + }, + }, + }, + } + + // Set DevfileRegistry instance as the owner and controller + ctrl.SetControllerReference(cr, pvc, scheme) + return pvc +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 0000000..f583872 --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,40 @@ +// +// Copyright (c) 2019-2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package util + +import ( + "crypto/tls" + "net/http" + "time" + + "k8s.io/apimachinery/pkg/util/wait" +) + +// Poll up to timeout seconds for pod to enter running state. +// Returns an error if the pod never enters the running state. +func WaitForServer(url string, timeout time.Duration) error { + return wait.PollImmediate(time.Second, timeout, func() (bool, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + resp, err := client.Get(url) + if err != nil { + return false, err + } + if resp.StatusCode == 200 { + return true, nil + } + return false, nil + }) +} diff --git a/registry.yaml b/registry.yaml new file mode 100644 index 0000000..d8e4195 --- /dev/null +++ b/registry.yaml @@ -0,0 +1,6 @@ +apiVersion: registry.devfile.io/v1alpha1 +kind: DevfileRegistry +metadata: + name: devfile-registry +spec: + devfileIndexImage: quay.io/devfile/devfile-index:next \ No newline at end of file diff --git a/tests/integration/cmd/devfileregistry_test.go b/tests/integration/cmd/devfileregistry_test.go new file mode 100644 index 0000000..d3714a2 --- /dev/null +++ b/tests/integration/cmd/devfileregistry_test.go @@ -0,0 +1,91 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/devfile/registry-operator/tests/integration/pkg/config" + "github.com/devfile/registry-operator/tests/integration/pkg/deploy" + "github.com/devfile/registry-operator/tests/integration/pkg/tests" + + "github.com/devfile/registry-operator/tests/integration/pkg/client" + _ "github.com/devfile/registry-operator/tests/integration/pkg/tests" + "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/gomega" +) + +// Integration/e2e test logic based on https://github.com/devfile/devworkspace-operator/tree/master/test/e2e + +//Create Constant file +const ( + testResultsDirectory = "/tmp/artifacts" + jUnitOutputFilename = "junit-devfileregistry-operator.xml" +) + +//SynchronizedBeforeSuite blocks is executed before run all test suites +var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { + fmt.Println("Starting to setup objects before run ginkgo suite") + namespace := os.Getenv("TEST_NAMESPACE") + if namespace != "" { + config.Namespace = namespace + } else { + config.Namespace = "registry-operator-system" + } + + k8sClient, err := client.NewK8sClient() + if err != nil { + fmt.Println("Failed to create k8s client") + panic(err) + } + + operator := deploy.NewDeployment(k8sClient) + + err = operator.CreateNamespace() + if err != nil { + panic(err) + } + + if err := operator.InstallCustomResourceDefinitions(); err != nil { + fmt.Println("Failed to install custom resources definitions on cluster") + panic(err) + } + + if err := operator.DeployDevfileRegistryOperator(); err != nil { + fmt.Println("Failed to deploy DevfileRegistry operator") + panic(err) + } + + tests.K8sClient, err = client.NewK8sClient() + if err != nil { + fmt.Println("Failed to create k8s client: " + err.Error()) + panic(err) + } + + return nil +}, func(data []byte) {}) + +func TestDevfileRegistryController(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + + fmt.Println("Creating ginkgo reporter for Test Harness: Junit and Debug Detail reporter") + var r []ginkgo.Reporter + r = append(r, reporters.NewJUnitReporter(filepath.Join(testResultsDirectory, jUnitOutputFilename))) + + fmt.Println("Running Devfile Registry integration tests...") + ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "Devfile Registry Operator Tests", r) +} diff --git a/tests/integration/examples/create/devfileregistry-tls.yaml b/tests/integration/examples/create/devfileregistry-tls.yaml new file mode 100644 index 0000000..e7f69d7 --- /dev/null +++ b/tests/integration/examples/create/devfileregistry-tls.yaml @@ -0,0 +1,10 @@ +apiVersion: registry.devfile.io/v1alpha1 +kind: DevfileRegistry +metadata: + name: devfileregistry-tls +spec: + devfileIndexImage: quay.io/devfile/devfile-index:next + storage: + enabled: false + tls: + enabled: true \ No newline at end of file diff --git a/tests/integration/examples/create/devfileregistry.yaml b/tests/integration/examples/create/devfileregistry.yaml new file mode 100644 index 0000000..2e306de --- /dev/null +++ b/tests/integration/examples/create/devfileregistry.yaml @@ -0,0 +1,10 @@ +apiVersion: registry.devfile.io/v1alpha1 +kind: DevfileRegistry +metadata: + name: devfileregistry +spec: + devfileIndexImage: quay.io/devfile/devfile-index:next + storage: + enabled: false + tls: + enabled: false \ No newline at end of file diff --git a/tests/integration/examples/update/devfileregistry-new.yaml b/tests/integration/examples/update/devfileregistry-new.yaml new file mode 100644 index 0000000..328038b --- /dev/null +++ b/tests/integration/examples/update/devfileregistry-new.yaml @@ -0,0 +1,10 @@ +apiVersion: registry.devfile.io/v1alpha1 +kind: DevfileRegistry +metadata: + name: devfileregistry-update +spec: + devfileIndexImage: quay.io/devfile/devfile-index:next + storage: + enabled: false + tls: + enabled: true \ No newline at end of file diff --git a/tests/integration/examples/update/devfileregistry-old.yaml b/tests/integration/examples/update/devfileregistry-old.yaml new file mode 100644 index 0000000..f1764cf --- /dev/null +++ b/tests/integration/examples/update/devfileregistry-old.yaml @@ -0,0 +1,10 @@ +apiVersion: registry.devfile.io/v1alpha1 +kind: DevfileRegistry +metadata: + name: devfileregistry-update +spec: + devfileIndexImage: quay.io/devfile/devfile-index:next + storage: + enabled: false + tls: + enabled: false \ No newline at end of file diff --git a/tests/integration/pkg/client/client.go b/tests/integration/pkg/client/client.go new file mode 100644 index 0000000..797917e --- /dev/null +++ b/tests/integration/pkg/client/client.go @@ -0,0 +1,54 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package client + +import ( + "fmt" + "os" + + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +type K8sClient struct { + kubeClient *kubernetes.Clientset + controllerClient client.Client +} + +// NewK8sClient creates kubernetes client wrapper with helper functions and direct access to k8s go client +func NewK8sClient() (*K8sClient, error) { + cfg, err := config.GetConfig() + if err != nil { + return nil, err + } + kubeClient, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + + // Instantiate an instance of conroller-runtime client + controllerClient, err := client.New(config.GetConfigOrDie(), client.Options{}) + if err != nil { + fmt.Println("failed to create client") + os.Exit(1) + } + + h := &K8sClient{kubeClient: kubeClient, controllerClient: controllerClient} + return h, nil +} + +// Kube returns the clientset for Kubernetes upstream. +func (c *K8sClient) Kube() kubernetes.Interface { + return c.kubeClient +} diff --git a/tests/integration/pkg/client/kubectl.go b/tests/integration/pkg/client/kubectl.go new file mode 100644 index 0000000..c1448ec --- /dev/null +++ b/tests/integration/pkg/client/kubectl.go @@ -0,0 +1,43 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package client + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/devfile/registry-operator/tests/integration/pkg/config" +) + +// KubectlApplyResource applies resources on the cluster, corresponding to the specified file(s) +func (w *K8sClient) KubectlApplyResource(filePath string) (err error) { + cmd := exec.Command("kubectl", "apply", "--namespace", config.Namespace, "-f", filePath) + outBytes, err := cmd.CombinedOutput() + output := string(outBytes) + if err != nil && !strings.Contains(output, "AlreadyExists") { + fmt.Println(err) + } + return err +} + +// KubectlDeleteResource deletes the resources from the cluster that the specified file(s) correspond to +func (w *K8sClient) KubectlDeleteResource(filePath string) (err error) { + cmd := exec.Command("kubectl", "delete", "--namespace", config.Namespace, "-f", filePath) + outBytes, err := cmd.CombinedOutput() + output := string(outBytes) + if err != nil && !strings.Contains(output, "AlreadyExists") { + fmt.Println(err) + } + return err +} diff --git a/tests/integration/pkg/client/pod.go b/tests/integration/pkg/client/pod.go new file mode 100644 index 0000000..cca9227 --- /dev/null +++ b/tests/integration/pkg/client/pod.go @@ -0,0 +1,103 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package client + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/devfile/registry-operator/tests/integration/pkg/config" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" +) + +// WaitForPodRunningByLabel waits for the pod matching the specified label to become running +// An error is returned if the pod does not exist or the timeout is reached +func (w *K8sClient) WaitForPodRunningByLabel(label string) (deployed bool, err error) { + timeout := time.After(6 * time.Minute) + tick := time.Tick(1 * time.Second) + + for { + select { + case <-timeout: + return false, errors.New("timed out") + case <-tick: + err := w.WaitForRunningPodBySelector(config.Namespace, label, 3*time.Minute) + if err == nil { + return true, nil + } + } + } +} + +// WaitForRunningPodBySelector waits up to timeout seconds for all pods in 'namespace' with given 'selector' to enter running state. +// Returns an error if no pods are found or not all discovered pods enter running state. +func (w *K8sClient) WaitForRunningPodBySelector(namespace, selector string, timeout time.Duration) error { + podList, err := w.ListPods(namespace, selector) + if err != nil { + return err + } + if len(podList.Items) == 0 { + fmt.Println("Pod not created yet with selector " + selector + " in namespace " + namespace) + + return fmt.Errorf("Pod not created yet in %s with label %s", namespace, selector) + } + + for _, pod := range podList.Items { + fmt.Println("Pod " + pod.Name + " created in namespace " + namespace + "...Checking startup data.") + if err := w.waitForPodRunning(namespace, pod.Name, timeout); err != nil { + return err + } + } + + return nil +} + +// ListPods returns the list of currently scheduled or running pods in `namespace` with the given selector +func (w *K8sClient) ListPods(namespace, selector string) (*v1.PodList, error) { + listOptions := metav1.ListOptions{LabelSelector: selector} + podList, err := w.Kube().CoreV1().Pods(namespace).List(context.TODO(), listOptions) + + if err != nil { + return nil, err + } + return podList, nil +} + +// Poll up to timeout seconds for pod to enter running state. +// Returns an error if the pod never enters the running state. +func (w *K8sClient) waitForPodRunning(namespace, podName string, timeout time.Duration) error { + return wait.PollImmediate(time.Second, timeout, w.isPodRunning(podName, namespace)) +} + +// return a condition function that indicates whether the given pod is +// currently running +func (w *K8sClient) isPodRunning(podName, namespace string) wait.ConditionFunc { + return func() (bool, error) { + pod, _ := w.Kube().CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{}) + age := time.Since(pod.GetCreationTimestamp().Time).Seconds() + + switch pod.Status.Phase { + case v1.PodRunning: + fmt.Println("Pod started after", age, "seconds") + return true, nil + case v1.PodFailed, v1.PodSucceeded: + return false, nil + } + return false, nil + } +} diff --git a/tests/integration/pkg/client/registry.go b/tests/integration/pkg/client/registry.go new file mode 100644 index 0000000..eeb03f1 --- /dev/null +++ b/tests/integration/pkg/client/registry.go @@ -0,0 +1,81 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package client + +import ( + "context" + "time" + + registryv1alpha1 "github.com/devfile/registry-operator/api/v1alpha1" + "github.com/devfile/registry-operator/tests/integration/pkg/config" + "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/util/wait" +) + +// GetRegistryInstance uses the Kubernetes REST API to retrieve the specified instance of the DevfileRegistry custom resource +// If there are any issues retrieving the resource or unmarshalling it, an error is returned +func (w *K8sClient) GetRegistryInstance(name string) (*registryv1alpha1.DevfileRegistry, error) { + data, err := w.kubeClient.RESTClient(). + Get(). + AbsPath("/apis/registry.devfile.io/v1alpha1"). + Namespace(config.Namespace). + Resource("devfileregistries"). + Name(name). + DoRaw(context.TODO()) + + if err != nil { + return nil, err + } + + // Unmarshall the struct + registry := ®istryv1alpha1.DevfileRegistry{} + err = yaml.Unmarshal(data, registry) + if err != nil { + return nil, err + } + + return registry, nil +} + +// WaitForRegistryInstance polls up to timeout seconds for the registry's server to become active (URL set in the status) +func (w *K8sClient) WaitForRegistryInstance(name string, timeout time.Duration) error { + return wait.PollImmediate(time.Second, timeout, func() (bool, error) { + devfileRegistry, err := w.GetRegistryInstance(name) + if err != nil { + return false, err + } + if devfileRegistry.Status.URL != "" { + return true, nil + } + return false, nil + }) +} + +// WaitForURLChange polls up to timeout seconds for the registry's URL to change in the status and returns it. +// If the URL doesn't change in the specified timeout, an error is returned +func (w *K8sClient) WaitForURLChange(name string, oldURL string, timeout time.Duration) (string, error) { + var newURL string + err := wait.PollImmediate(time.Second, timeout, func() (bool, error) { + devfileRegistry, err := w.GetRegistryInstance(name) + if err != nil { + return false, err + } + if devfileRegistry.Status.URL != oldURL { + newURL = devfileRegistry.Status.URL + return true, nil + } + return false, nil + }) + + return newURL, err +} diff --git a/tests/integration/pkg/config/config.go b/tests/integration/pkg/config/config.go new file mode 100644 index 0000000..c4edc3a --- /dev/null +++ b/tests/integration/pkg/config/config.go @@ -0,0 +1,15 @@ +// +// Copyright (c) 2019-2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package config + +var Namespace string diff --git a/tests/integration/pkg/deploy/controller.go b/tests/integration/pkg/deploy/controller.go new file mode 100644 index 0000000..db3230f --- /dev/null +++ b/tests/integration/pkg/deploy/controller.go @@ -0,0 +1,70 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package deploy + +import ( + "context" + "fmt" + "os/exec" + "strings" + + "k8s.io/apimachinery/pkg/api/errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/devfile/registry-operator/tests/integration/pkg/config" +) + +// CreateNamespace ensures that the namespace that the tests will run in already exiss +func (w *Deployment) CreateNamespace() error { + _, err := w.kubeClient.Kube().CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.Namespace, + }, + }, metav1.CreateOptions{}) + if errors.IsAlreadyExists(err) { + return nil + } + return err +} + +// DeployDevfileRegistryOperator deploys the DevfileRegistry operator +func (w *Deployment) DeployDevfileRegistryOperator() error { + label := "app.kubernetes.io/name=devfileregistry-operator" + cmd := exec.Command("make", "deploy") + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + if err != nil && !strings.Contains(string(output), "AlreadyExists") { + fmt.Println(err) + return err + } + + deploy, err := w.kubeClient.WaitForPodRunningByLabel(label) + fmt.Println("Devfile Regisry pod to be ready") + if !deploy || err != nil { + fmt.Println("Devfile Regisry not deployed") + return err + } + return nil +} + +func (w *Deployment) InstallCustomResourceDefinitions() error { + devfileRegistryCRD := exec.Command("make", "install") + output, err := devfileRegistryCRD.CombinedOutput() + if err != nil && !strings.Contains(string(output), "AlreadyExists") { + fmt.Println(err) + return err + } + return nil +} diff --git a/tests/integration/pkg/deploy/deploy.go b/tests/integration/pkg/deploy/deploy.go new file mode 100644 index 0000000..b5381e0 --- /dev/null +++ b/tests/integration/pkg/deploy/deploy.go @@ -0,0 +1,25 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package deploy + +import ( + "github.com/devfile/registry-operator/tests/integration/pkg/client" +) + +type Deployment struct { + kubeClient *client.K8sClient +} + +func NewDeployment(kubeClient *client.K8sClient) *Deployment { + return &Deployment{kubeClient: kubeClient} +} diff --git a/tests/integration/pkg/tests/devfileregistry_tests.go b/tests/integration/pkg/tests/devfileregistry_tests.go new file mode 100644 index 0000000..d5d7865 --- /dev/null +++ b/tests/integration/pkg/tests/devfileregistry_tests.go @@ -0,0 +1,145 @@ +// +// Copyright (c) 2020 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package tests + +import ( + "fmt" + "time" + + "github.com/devfile/registry-operator/pkg/util" + "github.com/devfile/registry-operator/tests/integration/pkg/client" + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" +) + +// Integration/e2e test logic based on https://github.com/devfile/devworkspace-operator/tree/master/test/e2e + +var K8sClient *client.K8sClient + +var _ = ginkgo.Describe("[Create Devfile Registry resource]", func() { + ginkgo.It("Should deploy a devfile registry on to the cluster", func() { + crName := "devfileregistry" + label := "devfileregistry_cr=" + crName + + // Deploy the devfileregistry resource for this test case and wait for the pod to be running + err := K8sClient.KubectlApplyResource("tests/integration/examples/create/devfileregistry.yaml") + if err != nil { + ginkgo.Fail("Failed to create devfileregistry instance: " + err.Error()) + return + } + deploy, err := K8sClient.WaitForPodRunningByLabel(label) + if !deploy { + fmt.Println("Devfile Registry didn't start properly") + } + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Wait for the registry instance to become ready + err = K8sClient.WaitForRegistryInstance(crName, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Retrieve the registry URL and verify the server is up and running + registry, err := K8sClient.GetRegistryInstance(crName) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = util.WaitForServer(registry.Status.URL, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + }) + + var _ = ginkgo.AfterEach(func() { + K8sClient.KubectlDeleteResource("tests/integration/examples/create/devfileregistry.yaml") + }) +}) + +var _ = ginkgo.Describe("[Create Devfile Registry resource with TLS enabled]", func() { + ginkgo.It("Should deploy a devfile registry on to the cluster with HTTPS", func() { + crName := "devfileregistry-tls" + label := "devfileregistry_cr=" + crName + + // Deploy the devfileregistry resource for this test case and wait for the pod to be running + err := K8sClient.KubectlApplyResource("tests/integration/examples/create/devfileregistry-tls.yaml") + if err != nil { + ginkgo.Fail("Failed to create devfileregistry instance: " + err.Error()) + return + } + deploy, err := K8sClient.WaitForPodRunningByLabel(label) + if !deploy { + fmt.Println("Devfile Registry didn't start properly") + } + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Wait for the registry instance to become ready + err = K8sClient.WaitForRegistryInstance(crName, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Retrieve the registry URL and verify that its protocol is https + registry, err := K8sClient.GetRegistryInstance(crName) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(registry.Status.URL).To(gomega.ContainSubstring("https://")) + + // Verify that the server is accessible. + err = util.WaitForServer(registry.Status.URL, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + }) + + var _ = ginkgo.AfterEach(func() { + K8sClient.KubectlDeleteResource("tests/integration/examples/create/devfileregistry-tls.yaml") + }) +}) + +var _ = ginkgo.Describe("[Update Devfile Registry resource]", func() { + ginkgo.It("Should deploy a devfile registry on to the cluster and properly update it", func() { + crName := "devfileregistry-update" + label := "devfileregistry_cr=" + crName + + // Deploy the devfileregistry resource for this test case and wait for the pod to be running + err := K8sClient.KubectlApplyResource("tests/integration/examples/update/devfileregistry-old.yaml") + if err != nil { + ginkgo.Fail("Failed to create devfileregistry instance: " + err.Error()) + return + } + deploy, err := K8sClient.WaitForPodRunningByLabel(label) + if !deploy { + fmt.Println("Devfile Registry didn't start properly") + } + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Wait for the registry instance to become ready + err = K8sClient.WaitForRegistryInstance(crName, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Retrieve the registry URL and verify the server is up and running + registry, err := K8sClient.GetRegistryInstance(crName) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = util.WaitForServer(registry.Status.URL, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + // Update the devfileregistry resource for this test case + err = K8sClient.KubectlApplyResource("tests/integration/examples/update/devfileregistry-new.yaml") + if err != nil { + ginkgo.Fail("Failed to create devfileregistry instance: " + err.Error()) + return + } + + // Retrieve the registry URL and verify that its protocol is https + url, err := K8sClient.WaitForURLChange(crName, registry.Status.URL, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(url).To(gomega.ContainSubstring("https://")) + + // Verify that the server is accessible. + err = util.WaitForServer(url, 30*time.Second) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + }) + + var _ = ginkgo.AfterEach(func() { + K8sClient.KubectlDeleteResource("tests/integration/examples/update/devfileregistry-new.yaml") + }) +})