github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/scaffold/olm-catalog/csv_updaters.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package catalog
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/operator-framework/operator-sdk/pkg/k8sutil"
    24  
    25  	"github.com/ghodss/yaml"
    26  	olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
    27  	olminstall "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
    28  	log "github.com/sirupsen/logrus"
    29  	appsv1 "k8s.io/api/apps/v1"
    30  	corev1 "k8s.io/api/core/v1"
    31  	rbacv1 "k8s.io/api/rbac/v1"
    32  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    33  )
    34  
    35  // CSVUpdater is an interface for any data that can be in a CSV, which will be
    36  // set to the corresponding field on Apply().
    37  type CSVUpdater interface {
    38  	// Apply applies a data update to a CSV argument.
    39  	Apply(*olmapiv1alpha1.ClusterServiceVersion) error
    40  }
    41  
    42  type updaterStore struct {
    43  	installStrategy *InstallStrategyUpdate
    44  	crds            *CustomResourceDefinitionsUpdate
    45  	almExamples     *ALMExamplesUpdate
    46  }
    47  
    48  func NewUpdaterStore() *updaterStore {
    49  	return &updaterStore{
    50  		installStrategy: &InstallStrategyUpdate{
    51  			&olminstall.StrategyDetailsDeployment{},
    52  		},
    53  		crds: &CustomResourceDefinitionsUpdate{
    54  			&olmapiv1alpha1.CustomResourceDefinitions{},
    55  			make(map[string]struct{}),
    56  		},
    57  		almExamples: &ALMExamplesUpdate{},
    58  	}
    59  }
    60  
    61  // Apply iteratively calls each stored CSVUpdater's Apply() method.
    62  func (s *updaterStore) Apply(csv *olmapiv1alpha1.ClusterServiceVersion) error {
    63  	updaters := []CSVUpdater{s.installStrategy, s.crds, s.almExamples}
    64  	for _, updater := range updaters {
    65  		if err := updater.Apply(csv); err != nil {
    66  			return err
    67  		}
    68  	}
    69  	return nil
    70  }
    71  
    72  func getKindfromYAML(yamlData []byte) (string, error) {
    73  	var temp struct {
    74  		Kind string
    75  	}
    76  	if err := yaml.Unmarshal(yamlData, &temp); err != nil {
    77  		return "", err
    78  	}
    79  	return temp.Kind, nil
    80  }
    81  
    82  func (s *updaterStore) AddToUpdater(yamlSpec []byte, kind string) (found bool, err error) {
    83  	found = true
    84  	switch kind {
    85  	case "Role":
    86  		err = s.AddRole(yamlSpec)
    87  	case "ClusterRole":
    88  		err = s.AddClusterRole(yamlSpec)
    89  	case "Deployment":
    90  		err = s.AddDeploymentSpec(yamlSpec)
    91  	case "CustomResourceDefinition":
    92  		// All CRD's present will be 'owned'.
    93  		err = s.AddOwnedCRD(yamlSpec)
    94  	default:
    95  		found = false
    96  	}
    97  	return found, err
    98  }
    99  
   100  type InstallStrategyUpdate struct {
   101  	*olminstall.StrategyDetailsDeployment
   102  }
   103  
   104  func (store *updaterStore) AddRole(yamlDoc []byte) error {
   105  	role := &rbacv1.Role{}
   106  	if err := yaml.Unmarshal(yamlDoc, role); err != nil {
   107  		return err
   108  	}
   109  	perm := olminstall.StrategyDeploymentPermissions{
   110  		ServiceAccountName: role.ObjectMeta.Name,
   111  		Rules:              role.Rules,
   112  	}
   113  	store.installStrategy.Permissions = append(store.installStrategy.Permissions, perm)
   114  
   115  	return nil
   116  }
   117  
   118  func (store *updaterStore) AddClusterRole(yamlDoc []byte) error {
   119  	clusterRole := &rbacv1.ClusterRole{}
   120  	if err := yaml.Unmarshal(yamlDoc, clusterRole); err != nil {
   121  		return err
   122  	}
   123  	perm := olminstall.StrategyDeploymentPermissions{
   124  		ServiceAccountName: clusterRole.ObjectMeta.Name,
   125  		Rules:              clusterRole.Rules,
   126  	}
   127  	store.installStrategy.ClusterPermissions = append(store.installStrategy.ClusterPermissions, perm)
   128  
   129  	return nil
   130  }
   131  
   132  const olmTNMeta = "metadata.annotations['olm.targetNamespaces']"
   133  
   134  func (store *updaterStore) AddDeploymentSpec(yamlDoc []byte) error {
   135  	dep := &appsv1.Deployment{}
   136  	if err := yaml.Unmarshal(yamlDoc, dep); err != nil {
   137  		return err
   138  	}
   139  
   140  	setWatchNamespacesEnv(dep)
   141  	// Make sure "olm.targetNamespaces" is referenced somewhere in dep,
   142  	// and emit a warning of not.
   143  	if !depHasOLMNamespaces(dep) {
   144  		log.Warnf(`No WATCH_NAMESPACE environment variable nor reference to "%s"`+
   145  			` detected in operator Deployment. For OLM compatibility, your operator`+
   146  			` MUST watch namespaces defined in "%s"`, olmTNMeta, olmTNMeta)
   147  	}
   148  
   149  	depSpec := olminstall.StrategyDeploymentSpec{
   150  		Name: dep.ObjectMeta.Name,
   151  		Spec: dep.Spec,
   152  	}
   153  	store.installStrategy.DeploymentSpecs = append(store.installStrategy.DeploymentSpecs, depSpec)
   154  
   155  	return nil
   156  }
   157  
   158  // setWatchNamespacesEnv sets WATCH_NAMESPACE to olmTNString in dep if
   159  // WATCH_NAMESPACE exists in a pod spec container's env list.
   160  func setWatchNamespacesEnv(dep *appsv1.Deployment) {
   161  	overwriteContainerEnvVar(dep, k8sutil.WatchNamespaceEnvVar, newEnvVar(k8sutil.WatchNamespaceEnvVar, olmTNMeta))
   162  }
   163  
   164  func overwriteContainerEnvVar(dep *appsv1.Deployment, name string, ev corev1.EnvVar) {
   165  	for _, c := range dep.Spec.Template.Spec.Containers {
   166  		for i := 0; i < len(c.Env); i++ {
   167  			if c.Env[i].Name == name {
   168  				c.Env[i] = ev
   169  			}
   170  		}
   171  	}
   172  }
   173  
   174  func newEnvVar(name, fieldPath string) corev1.EnvVar {
   175  	return corev1.EnvVar{
   176  		Name: name,
   177  		ValueFrom: &corev1.EnvVarSource{
   178  			FieldRef: &corev1.ObjectFieldSelector{
   179  				FieldPath: fieldPath,
   180  			},
   181  		},
   182  	}
   183  }
   184  
   185  // OLM places the set of target namespaces for the operator in
   186  // "metadata.annotations['olm.targetNamespaces']". This value should be
   187  // referenced in either:
   188  //	- The Deployment's pod spec WATCH_NAMESPACE env variable.
   189  //	- Some other Deployment pod spec field.
   190  func depHasOLMNamespaces(dep *appsv1.Deployment) bool {
   191  	b, err := dep.Spec.Template.Marshal()
   192  	if err != nil {
   193  		// Something is wrong with the deployment manifest, not with CLI inputs.
   194  		log.Fatalf("marshal Deployment spec: %v", err)
   195  	}
   196  	return bytes.Index(b, []byte(olmTNMeta)) != -1
   197  }
   198  
   199  func (u *InstallStrategyUpdate) Apply(csv *olmapiv1alpha1.ClusterServiceVersion) (err error) {
   200  	// Get install strategy from csv. Default to a deployment strategy if none found.
   201  	var strat olminstall.Strategy
   202  	if csv.Spec.InstallStrategy.StrategyName == "" {
   203  		csv.Spec.InstallStrategy.StrategyName = olminstall.InstallStrategyNameDeployment
   204  		strat = &olminstall.StrategyDetailsDeployment{}
   205  	} else {
   206  		var resolver *olminstall.StrategyResolver
   207  		strat, err = resolver.UnmarshalStrategy(csv.Spec.InstallStrategy)
   208  		if err != nil {
   209  			return err
   210  		}
   211  	}
   212  
   213  	switch s := strat.(type) {
   214  	case *olminstall.StrategyDetailsDeployment:
   215  		// Update permissions and deployments.
   216  		u.updatePermissions(s)
   217  		u.updateClusterPermissions(s)
   218  		u.updateDeploymentSpecs(s)
   219  	default:
   220  		return fmt.Errorf("install strategy (%v) of unknown type", strat)
   221  	}
   222  
   223  	// Re-serialize permissions into csv strategy.
   224  	updatedStrat, err := json.Marshal(strat)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	csv.Spec.InstallStrategy.StrategySpecRaw = updatedStrat
   229  
   230  	return nil
   231  }
   232  
   233  func (u *InstallStrategyUpdate) updatePermissions(strat *olminstall.StrategyDetailsDeployment) {
   234  	if len(u.Permissions) != 0 {
   235  		strat.Permissions = u.Permissions
   236  	}
   237  }
   238  
   239  func (u *InstallStrategyUpdate) updateClusterPermissions(strat *olminstall.StrategyDetailsDeployment) {
   240  	if len(u.ClusterPermissions) != 0 {
   241  		strat.ClusterPermissions = u.ClusterPermissions
   242  	}
   243  }
   244  
   245  func (u *InstallStrategyUpdate) updateDeploymentSpecs(strat *olminstall.StrategyDetailsDeployment) {
   246  	if len(u.DeploymentSpecs) != 0 {
   247  		strat.DeploymentSpecs = u.DeploymentSpecs
   248  	}
   249  }
   250  
   251  type CustomResourceDefinitionsUpdate struct {
   252  	*olmapiv1alpha1.CustomResourceDefinitions
   253  	crKinds map[string]struct{}
   254  }
   255  
   256  func (store *updaterStore) AddOwnedCRD(yamlDoc []byte) error {
   257  	crd := &apiextv1beta1.CustomResourceDefinition{}
   258  	if err := yaml.Unmarshal(yamlDoc, crd); err != nil {
   259  		return err
   260  	}
   261  	store.crds.Owned = append(store.crds.Owned, olmapiv1alpha1.CRDDescription{
   262  		Name:    crd.ObjectMeta.Name,
   263  		Version: crd.Spec.Version,
   264  		Kind:    crd.Spec.Names.Kind,
   265  	})
   266  	store.crds.crKinds[crd.Spec.Names.Kind] = struct{}{}
   267  	return nil
   268  }
   269  
   270  // Apply updates csv's "owned" CRDDescriptions. "required" CRDDescriptions are
   271  // left as-is, since they are user-defined values.
   272  func (u *CustomResourceDefinitionsUpdate) Apply(csv *olmapiv1alpha1.ClusterServiceVersion) error {
   273  	set := make(map[string]olmapiv1alpha1.CRDDescription)
   274  	for _, csvDesc := range csv.Spec.CustomResourceDefinitions.Owned {
   275  		set[csvDesc.Name] = csvDesc
   276  	}
   277  	du := u.DeepCopy()
   278  	for i, uDesc := range u.Owned {
   279  		if csvDesc, ok := set[uDesc.Name]; ok {
   280  			csvDesc.Name = uDesc.Name
   281  			csvDesc.Version = uDesc.Version
   282  			csvDesc.Kind = uDesc.Kind
   283  			du.Owned[i] = csvDesc
   284  		}
   285  	}
   286  	csv.Spec.CustomResourceDefinitions.Owned = du.Owned
   287  	return nil
   288  }
   289  
   290  type ALMExamplesUpdate struct {
   291  	crs []string
   292  }
   293  
   294  func (store *updaterStore) AddCR(yamlDoc []byte) error {
   295  	if len(yamlDoc) == 0 {
   296  		return nil
   297  	}
   298  	crBytes, err := yaml.YAMLToJSON(yamlDoc)
   299  	if err != nil {
   300  		return err
   301  	}
   302  	store.almExamples.crs = append(store.almExamples.crs, string(crBytes))
   303  	return nil
   304  }
   305  
   306  func (u *ALMExamplesUpdate) Apply(csv *olmapiv1alpha1.ClusterServiceVersion) error {
   307  	if len(u.crs) == 0 {
   308  		return nil
   309  	}
   310  	if csv.GetAnnotations() == nil {
   311  		csv.SetAnnotations(make(map[string]string))
   312  	}
   313  	sb := &strings.Builder{}
   314  	sb.WriteString(`[`)
   315  	for i, example := range u.crs {
   316  		sb.WriteString(example)
   317  		if i < len(u.crs)-1 {
   318  			sb.WriteString(`,`)
   319  		}
   320  	}
   321  	sb.WriteString(`]`)
   322  
   323  	csv.GetAnnotations()["alm-examples"] = sb.String()
   324  	return nil
   325  }