441 lines
15 KiB
Go
441 lines
15 KiB
Go
//
|
|
// Copyright (c) 2019-2021 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 imagepuller
|
|
|
|
import (
|
|
"context"
|
|
goerror "errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/sirupsen/logrus"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
|
|
chev1alpha1 "github.com/che-incubator/kubernetes-image-puller-operator/api/v1alpha1"
|
|
"github.com/eclipse-che/che-operator/pkg/common/chetypes"
|
|
"github.com/eclipse-che/che-operator/pkg/common/constants"
|
|
"github.com/eclipse-che/che-operator/pkg/common/utils"
|
|
"github.com/eclipse-che/che-operator/pkg/deploy"
|
|
operatorsv1 "github.com/operator-framework/api/pkg/operators/v1"
|
|
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
|
|
packagesv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/operators/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
)
|
|
|
|
var (
|
|
log = ctrl.Log.WithName("image-puller")
|
|
defaultImagePatterns = [...]string{
|
|
"^RELATED_IMAGE_.*_theia.*",
|
|
"^RELATED_IMAGE_.*_code.*",
|
|
"^RELATED_IMAGE_.*_idea.*",
|
|
"^RELATED_IMAGE_.*_machine(_)?exec(_.*)?_plugin_registry_image.*",
|
|
"^RELATED_IMAGE_.*_kubernetes(_.*)?_plugin_registry_image.*",
|
|
"^RELATED_IMAGE_.*_openshift(_.*)?_plugin_registry_image.*",
|
|
"^RELATED_IMAGE_universal(_)?developer(_)?image(_.*)?_devfile_registry_image.*",
|
|
}
|
|
kubernetesImagePullerDiffOpts = cmp.Options{
|
|
cmpopts.IgnoreFields(chev1alpha1.KubernetesImagePuller{}, "TypeMeta", "ObjectMeta", "Status"),
|
|
}
|
|
)
|
|
|
|
const (
|
|
subscriptionName = "kubernetes-imagepuller-operator"
|
|
operatorGroupName = "kubernetes-imagepuller-operator"
|
|
packageName = "kubernetes-imagepuller-operator"
|
|
componentName = "kubernetes-image-puller"
|
|
imagePullerFinalizerName = "kubernetesimagepullers.finalizers.che.eclipse.org"
|
|
defaultConfigMapName = "k8s-image-puller"
|
|
defaultDeploymentName = "kubernetes-image-puller"
|
|
defaultImagePullerImage = "quay.io/eclipse/kubernetes-image-puller:next"
|
|
)
|
|
|
|
type Images2Pull = map[string]string
|
|
|
|
type ImagePuller struct {
|
|
deploy.Reconcilable
|
|
}
|
|
|
|
func NewImagePuller() *ImagePuller {
|
|
return &ImagePuller{}
|
|
}
|
|
|
|
func (ip *ImagePuller) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) {
|
|
if ctx.CheCluster.Spec.Components.ImagePuller.Enable {
|
|
if foundPackagesAPI, foundOperatorsAPI, _, err := ip.discoverImagePullerApis(ctx); !foundPackagesAPI || !foundOperatorsAPI {
|
|
if err != nil {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
errorMsg := "couldn't find Operator Lifecycle Manager types to install the Kubernetes Image Puller Operator. Please install Operator Lifecycle Manager to install the operator or disable the image puller by setting `spec.imagePuller.enable` to false"
|
|
return reconcile.Result{RequeueAfter: time.Second}, false, fmt.Errorf(errorMsg)
|
|
}
|
|
|
|
if err := deploy.AppendFinalizer(ctx, imagePullerFinalizerName); err != nil {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
|
|
if done, err := ip.syncOperatorGroup(ctx); !done {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
|
|
if done, err := ip.syncSubscription(ctx); !done {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
|
|
if done, err := ip.syncDefaultImages(ctx); !done {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
|
|
// Wait for KubernetesImagePuller API
|
|
if _, _, foundKubernetesImagePullerAPI, err := ip.discoverImagePullerApis(ctx); !foundKubernetesImagePullerAPI {
|
|
if err != nil {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
logrus.Infof("Waiting 15 seconds for kubernetesimagepullers.che.eclipse.org API")
|
|
return reconcile.Result{RequeueAfter: 15 * time.Second}, false, nil
|
|
}
|
|
|
|
if done, err := ip.syncKubernetesImagePuller(ctx); !done {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
} else {
|
|
if done, err := ip.uninstallImagePullerOperator(ctx); !done {
|
|
return reconcile.Result{}, false, err
|
|
}
|
|
}
|
|
return reconcile.Result{}, true, nil
|
|
}
|
|
|
|
func (ip *ImagePuller) Finalize(ctx *chetypes.DeployContext) bool {
|
|
done, err := ip.uninstallImagePullerOperator(ctx)
|
|
if err != nil {
|
|
log.Error(err, "Failed to uninstall KubernetesImagePuller")
|
|
}
|
|
return done
|
|
}
|
|
|
|
// Uninstall the CSV, OperatorGroup, Subscription, KubernetesImagePuller, and update the CheCluster to remove
|
|
// the image puller spec. Returns true if the CheCluster was updated
|
|
func (ip *ImagePuller) uninstallImagePullerOperator(ctx *chetypes.DeployContext) (bool, error) {
|
|
_, foundOperatorsAPI, foundKubernetesImagePullerAPI, err := ip.discoverImagePullerApis(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if foundKubernetesImagePullerAPI {
|
|
if done, err := deploy.DeleteByKeyWithClient(
|
|
ctx.ClusterAPI.NonCachingClient,
|
|
types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: getImagePullerOperatorName(ctx)},
|
|
&chev1alpha1.KubernetesImagePuller{},
|
|
); !done {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if foundOperatorsAPI {
|
|
// Delete the Subscription and ClusterServiceVersion
|
|
subscription := &operatorsv1alpha1.Subscription{}
|
|
if exists, err := deploy.GetWithClient(
|
|
ctx.ClusterAPI.NonCachingClient,
|
|
types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: subscriptionName},
|
|
subscription,
|
|
); err != nil {
|
|
return false, err
|
|
} else if exists {
|
|
if subscription.Status.InstalledCSV != "" {
|
|
if done, err := deploy.DeleteByKeyWithClient(
|
|
ctx.ClusterAPI.NonCachingClient,
|
|
types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: subscription.Status.InstalledCSV},
|
|
&operatorsv1alpha1.ClusterServiceVersion{}); !done {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if done, err := deploy.DeleteByKeyWithClient(
|
|
ctx.ClusterAPI.NonCachingClient,
|
|
types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: subscriptionName},
|
|
&operatorsv1alpha1.Subscription{}); !done {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
// Delete the OperatorGroup
|
|
if done, err := deploy.DeleteByKeyWithClient(
|
|
ctx.ClusterAPI.NonCachingClient,
|
|
types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: operatorGroupName},
|
|
&operatorsv1.OperatorGroup{},
|
|
); !done {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if err := deploy.DeleteFinalizer(ctx, imagePullerFinalizerName); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// CheckNeededImagePullerApis check if the API server can discover the API groups
|
|
// for packages.operators.coreos.com, operators.coreos.com, and che.eclipse.org.
|
|
// Returns:
|
|
// foundPackagesAPI - true if the server discovers the packages.operators.coreos.com API
|
|
// foundOperatorsAPI - true if the server discovers the operators.coreos.com API
|
|
// foundKubernetesImagePullerAPI - true if the server discovers the che.eclipse.org API
|
|
// error - any error returned by the call to discoveryClient.ServerGroups()
|
|
func (ip *ImagePuller) discoverImagePullerApis(ctx *chetypes.DeployContext) (bool, bool, bool, error) {
|
|
groupList, resourcesList, err := ctx.ClusterAPI.DiscoveryClient.ServerGroupsAndResources()
|
|
if err != nil {
|
|
return false, false, false, err
|
|
}
|
|
foundPackagesAPI := false
|
|
foundOperatorsAPI := false
|
|
foundKubernetesImagePullerAPI := false
|
|
for _, group := range groupList {
|
|
if group.Name == packagesv1.SchemeGroupVersion.Group {
|
|
foundPackagesAPI = true
|
|
}
|
|
if group.Name == operatorsv1alpha1.SchemeGroupVersion.Group {
|
|
foundOperatorsAPI = true
|
|
}
|
|
}
|
|
|
|
for _, l := range resourcesList {
|
|
for _, r := range l.APIResources {
|
|
if l.GroupVersion == chev1alpha1.SchemeBuilder.GroupVersion.String() && r.Kind == "KubernetesImagePuller" {
|
|
foundKubernetesImagePullerAPI = true
|
|
}
|
|
}
|
|
}
|
|
return foundPackagesAPI, foundOperatorsAPI, foundKubernetesImagePullerAPI, nil
|
|
}
|
|
|
|
func (ip *ImagePuller) syncDefaultImages(ctx *chetypes.DeployContext) (bool, error) {
|
|
defaultImages := getDefaultImages()
|
|
specImages := stringToImages(ctx.CheCluster.Spec.Components.ImagePuller.Spec.Images)
|
|
|
|
if len(specImages) == 0 {
|
|
specImages = defaultImages
|
|
} else {
|
|
for specImageName, specImage := range specImages {
|
|
for defaultImageName, defaultImage := range defaultImages {
|
|
if specImageName == defaultImageName && specImage != defaultImage {
|
|
specImages[specImageName] = defaultImage
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
specImagesAsString := imagesToString(specImages)
|
|
if ctx.CheCluster.Spec.Components.ImagePuller.Spec.Images != specImagesAsString {
|
|
ctx.CheCluster.Spec.Components.ImagePuller.Spec.Images = specImagesAsString
|
|
err := deploy.UpdateCheCRSpec(ctx, "components.imagePuller.spec.images ", specImagesAsString)
|
|
return err == nil, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (ip *ImagePuller) syncOperatorGroup(ctx *chetypes.DeployContext) (bool, error) {
|
|
operatorGroupList := &operatorsv1.OperatorGroupList{}
|
|
if err := ctx.ClusterAPI.NonCachingClient.List(context.TODO(), operatorGroupList, &client.ListOptions{Namespace: ctx.CheCluster.Namespace}); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(operatorGroupList.Items) != 0 {
|
|
return true, nil
|
|
}
|
|
|
|
operatorGroup := &operatorsv1.OperatorGroup{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "OperatorGroup",
|
|
APIVersion: operatorsv1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: operatorGroupName,
|
|
Namespace: ctx.CheCluster.Namespace,
|
|
Labels: map[string]string{
|
|
constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg,
|
|
constants.KubernetesComponentLabelKey: componentName,
|
|
constants.KubernetesManagedByLabelKey: deploy.GetManagedByLabel(),
|
|
},
|
|
},
|
|
Spec: operatorsv1.OperatorGroupSpec{
|
|
TargetNamespaces: []string{},
|
|
},
|
|
}
|
|
|
|
_, err := deploy.CreateIfNotExistsWithClient(ctx.ClusterAPI.NonCachingClient, ctx, operatorGroup)
|
|
return err == nil, err
|
|
}
|
|
|
|
func (ip *ImagePuller) syncSubscription(ctx *chetypes.DeployContext) (bool, error) {
|
|
packageManifest := &packagesv1.PackageManifest{}
|
|
if exists, err := deploy.GetWithClient(ctx.ClusterAPI.NonCachingClient, types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: packageName}, packageManifest); !exists {
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return false, fmt.Errorf("there is no PackageManifest for the Kubernetes Image Puller Operator. Install the Operator Lifecycle Manager and the Community Operators Catalog")
|
|
}
|
|
|
|
subscription := &operatorsv1alpha1.Subscription{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "Subscription",
|
|
APIVersion: operatorsv1alpha1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: subscriptionName,
|
|
Namespace: ctx.CheCluster.Namespace,
|
|
Labels: map[string]string{
|
|
constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg,
|
|
constants.KubernetesComponentLabelKey: componentName,
|
|
constants.KubernetesManagedByLabelKey: deploy.GetManagedByLabel(),
|
|
},
|
|
},
|
|
Spec: &operatorsv1alpha1.SubscriptionSpec{
|
|
CatalogSource: packageManifest.Status.CatalogSource,
|
|
CatalogSourceNamespace: packageManifest.Status.CatalogSourceNamespace,
|
|
Channel: packageManifest.Status.DefaultChannel,
|
|
InstallPlanApproval: operatorsv1alpha1.ApprovalAutomatic,
|
|
Package: packageName,
|
|
},
|
|
}
|
|
|
|
_, err := deploy.CreateIfNotExistsWithClient(ctx.ClusterAPI.NonCachingClient, ctx, subscription)
|
|
return err == nil, err
|
|
}
|
|
|
|
func (ip *ImagePuller) syncKubernetesImagePuller(ctx *chetypes.DeployContext) (bool, error) {
|
|
imagePuller := &chev1alpha1.KubernetesImagePuller{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: chev1alpha1.GroupVersion.String(),
|
|
Kind: "KubernetesImagePuller",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: getImagePullerOperatorName(ctx),
|
|
Namespace: ctx.CheCluster.Namespace,
|
|
Labels: map[string]string{
|
|
constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg,
|
|
constants.KubernetesComponentLabelKey: componentName,
|
|
constants.KubernetesManagedByLabelKey: deploy.GetManagedByLabel(),
|
|
},
|
|
},
|
|
Spec: ctx.CheCluster.Spec.Components.ImagePuller.Spec,
|
|
}
|
|
|
|
// Set default values to avoid syncing object on every loop
|
|
// See https://github.com/che-incubator/kubernetes-image-puller-operator/blob/main/controllers/kubernetesimagepuller_controller.go
|
|
imagePuller.Spec.ConfigMapName = utils.GetValue(imagePuller.Spec.ConfigMapName, defaultConfigMapName)
|
|
imagePuller.Spec.DeploymentName = utils.GetValue(imagePuller.Spec.DeploymentName, defaultDeploymentName)
|
|
imagePuller.Spec.ImagePullerImage = utils.GetValue(imagePuller.Spec.ImagePullerImage, defaultImagePullerImage)
|
|
|
|
return deploy.SyncWithClient(ctx.ClusterAPI.NonCachingClient, ctx, imagePuller, kubernetesImagePullerDiffOpts)
|
|
}
|
|
|
|
func getImagePullerOperatorName(ctx *chetypes.DeployContext) string {
|
|
return ctx.CheCluster.Name + "-image-puller"
|
|
}
|
|
|
|
// imagesToString returns a string representation of the provided image slice,
|
|
// suitable for the imagePuller.spec.images field
|
|
func imagesToString(images Images2Pull) string {
|
|
imageNames := make([]string, 0, len(images))
|
|
for k := range images {
|
|
imageNames = append(imageNames, k)
|
|
}
|
|
sort.Strings(imageNames)
|
|
|
|
imagesAsString := ""
|
|
for _, imageName := range imageNames {
|
|
if name, err := convertToRFC1123(imageName); err == nil {
|
|
imagesAsString += name + "=" + images[imageName] + ";"
|
|
}
|
|
}
|
|
return imagesAsString
|
|
}
|
|
|
|
// stringToImages returns a slice of ImageAndName structs from the provided semi-colon seperated string
|
|
// of key value pairs
|
|
func stringToImages(imagesString string) Images2Pull {
|
|
currentImages := strings.Split(imagesString, ";")
|
|
for i, image := range currentImages {
|
|
currentImages[i] = strings.TrimSpace(image)
|
|
}
|
|
// Remove the last element, if empty
|
|
if currentImages[len(currentImages)-1] == "" {
|
|
currentImages = currentImages[:len(currentImages)-1]
|
|
}
|
|
|
|
images := map[string]string{}
|
|
for _, image := range currentImages {
|
|
nameAndImage := strings.Split(image, "=")
|
|
if len(nameAndImage) != 2 {
|
|
logrus.Warnf("Malformed image name/tag: %s. Ignoring.", image)
|
|
continue
|
|
}
|
|
images[nameAndImage[0]] = nameAndImage[1]
|
|
}
|
|
|
|
return images
|
|
}
|
|
|
|
// convertToRFC1123 converts input string to RFC 1123 format ([a-z0-9]([-a-z0-9]*[a-z0-9])?) max 63 characters, if possible
|
|
func convertToRFC1123(str string) (string, error) {
|
|
result := strings.ToLower(str)
|
|
if len(str) > validation.DNS1123LabelMaxLength {
|
|
result = result[:validation.DNS1123LabelMaxLength]
|
|
}
|
|
|
|
// Remove illegal trailing characters
|
|
i := len(result) - 1
|
|
for i >= 0 && !isRFC1123Char(result[i]) {
|
|
i -= 1
|
|
}
|
|
result = result[:i+1]
|
|
|
|
result = strings.ReplaceAll(result, "_", "-")
|
|
|
|
if errs := validation.IsDNS1123Label(result); len(errs) > 0 {
|
|
return "", goerror.New("Cannot convert the following string to RFC 1123 format: " + str)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func isRFC1123Char(ch byte) bool {
|
|
errs := validation.IsDNS1123Label(string(ch))
|
|
return len(errs) == 0
|
|
}
|
|
|
|
// GetDefaultImages returns the current default images from the environment variables
|
|
func getDefaultImages() Images2Pull {
|
|
images := map[string]string{}
|
|
for _, pattern := range defaultImagePatterns {
|
|
matches := utils.GetEnvsByRegExp(pattern)
|
|
sort.SliceStable(matches, func(i, j int) bool {
|
|
return strings.Compare(matches[i].Name, matches[j].Name) < 0
|
|
})
|
|
|
|
for _, match := range matches {
|
|
images[match.Name[len("RELATED_IMAGE_"):]] = match.Value
|
|
}
|
|
}
|
|
return images
|
|
}
|