github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/internal/pkg/scaffold/olm-catalog/csv.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  	"errors"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"sync"
    25  	"unicode"
    26  
    27  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
    28  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input"
    29  	"github.com/operator-framework/operator-sdk/internal/util/k8sutil"
    30  	"github.com/operator-framework/operator-sdk/internal/util/yamlutil"
    31  
    32  	"github.com/coreos/go-semver/semver"
    33  	"github.com/ghodss/yaml"
    34  	olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
    35  	log "github.com/sirupsen/logrus"
    36  	"github.com/spf13/afero"
    37  )
    38  
    39  const (
    40  	CSVYamlFileExt    = ".clusterserviceversion.yaml"
    41  	CSVConfigYamlFile = "csv-config.yaml"
    42  )
    43  
    44  var ErrNoCSVVersion = errors.New("no CSV version supplied")
    45  
    46  type CSV struct {
    47  	input.Input
    48  
    49  	// ConfigFilePath is the location of a configuration file path for this
    50  	// projects' CSV file.
    51  	ConfigFilePath string
    52  	// CSVVersion is the CSV current version.
    53  	CSVVersion string
    54  	// FromVersion is the CSV version from which to build a new CSV. A CSV
    55  	// manifest with this version should exist at:
    56  	// deploy/olm-catalog/{from_version}/operator-name.v{from_version}.{CSVYamlFileExt}
    57  	FromVersion string
    58  
    59  	once       sync.Once
    60  	fs         afero.Fs // For testing, ex. afero.NewMemMapFs()
    61  	pathPrefix string   // For testing, ex. testdata/deploy/olm-catalog
    62  }
    63  
    64  func (s *CSV) initFS(fs afero.Fs) {
    65  	s.once.Do(func() {
    66  		s.fs = fs
    67  	})
    68  }
    69  
    70  func (s *CSV) getFS() afero.Fs {
    71  	s.initFS(afero.NewOsFs())
    72  	return s.fs
    73  }
    74  
    75  func (s *CSV) GetInput() (input.Input, error) {
    76  	// A CSV version is required.
    77  	if s.CSVVersion == "" {
    78  		return input.Input{}, ErrNoCSVVersion
    79  	}
    80  	if s.Path == "" {
    81  		lowerProjName := strings.ToLower(s.ProjectName)
    82  		// Path is what the operator-registry expects:
    83  		// {manifests -> olm-catalog}/{operator_name}/{semver}/{operator_name}.v{semver}.clusterserviceversion.yaml
    84  		s.Path = filepath.Join(s.pathPrefix,
    85  			scaffold.OLMCatalogDir,
    86  			lowerProjName,
    87  			s.CSVVersion,
    88  			getCSVFileName(lowerProjName, s.CSVVersion),
    89  		)
    90  	}
    91  	if s.ConfigFilePath == "" {
    92  		s.ConfigFilePath = filepath.Join(s.pathPrefix, scaffold.OLMCatalogDir, CSVConfigYamlFile)
    93  	}
    94  	return s.Input, nil
    95  }
    96  
    97  func (s *CSV) SetFS(fs afero.Fs) { s.initFS(fs) }
    98  
    99  // CustomRender allows a CSV to be written by marshalling
   100  // olmapiv1alpha1.ClusterServiceVersion instead of writing to a template.
   101  func (s *CSV) CustomRender() ([]byte, error) {
   102  	s.initFS(afero.NewOsFs())
   103  
   104  	// Get current CSV to update.
   105  	csv, exists, err := s.getBaseCSVIfExists()
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	if !exists {
   110  		csv = &olmapiv1alpha1.ClusterServiceVersion{}
   111  		s.initCSVFields(csv)
   112  	}
   113  
   114  	cfg, err := GetCSVConfig(s.ConfigFilePath)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	setCSVDefaultFields(csv)
   120  	if err = s.updateCSVVersions(csv); err != nil {
   121  		return nil, err
   122  	}
   123  	if err = s.updateCSVFromManifestFiles(cfg, csv); err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	if fields := getEmptyRequiredCSVFields(csv); len(fields) != 0 {
   128  		if exists {
   129  			log.Warnf("Required csv fields not filled in file %s:%s\n", s.Path, joinFields(fields))
   130  		} else {
   131  			// A new csv won't have several required fields populated.
   132  			// Report required fields to user informationally.
   133  			log.Infof("Fill in the following required fields in file %s:%s\n", s.Path, joinFields(fields))
   134  		}
   135  	}
   136  
   137  	return k8sutil.GetObjectBytes(csv)
   138  }
   139  
   140  func (s *CSV) getBaseCSVIfExists() (*olmapiv1alpha1.ClusterServiceVersion, bool, error) {
   141  	verToGet := s.CSVVersion
   142  	if s.FromVersion != "" {
   143  		verToGet = s.FromVersion
   144  	}
   145  	csv, exists, err := getCSVFromFSIfExists(s.getFS(), s.getCSVPath(verToGet))
   146  	if err != nil {
   147  		return nil, false, err
   148  	}
   149  	if !exists && s.FromVersion != "" {
   150  		log.Warnf("FromVersion set (%s) but CSV does not exist", s.FromVersion)
   151  	}
   152  	return csv, exists, nil
   153  }
   154  
   155  func getCSVFromFSIfExists(fs afero.Fs, path string) (*olmapiv1alpha1.ClusterServiceVersion, bool, error) {
   156  	csvBytes, err := afero.ReadFile(fs, path)
   157  	if err != nil {
   158  		if os.IsNotExist(err) {
   159  			return nil, false, nil
   160  		}
   161  		return nil, false, err
   162  	}
   163  	if len(csvBytes) == 0 {
   164  		return nil, false, nil
   165  	}
   166  
   167  	csv := &olmapiv1alpha1.ClusterServiceVersion{}
   168  	if err := yaml.Unmarshal(csvBytes, csv); err != nil {
   169  		return nil, false, err
   170  	}
   171  
   172  	return csv, true, nil
   173  }
   174  
   175  func getCSVName(name, version string) string {
   176  	return name + ".v" + version
   177  }
   178  
   179  func getCSVFileName(name, version string) string {
   180  	return getCSVName(name, version) + CSVYamlFileExt
   181  }
   182  
   183  func (s *CSV) getCSVPath(ver string) string {
   184  	lowerProjName := strings.ToLower(s.ProjectName)
   185  	name := getCSVFileName(lowerProjName, ver)
   186  	return filepath.Join(s.pathPrefix, scaffold.OLMCatalogDir, lowerProjName, ver, name)
   187  }
   188  
   189  // getDisplayName turns a project dir name in any of {snake, chain, camel}
   190  // cases, hierarchical dot structure, or space-delimited into a
   191  // space-delimited, title'd display name.
   192  // Ex. "another-_AppOperator_againTwiceThrice More"
   193  // ->  "Another App Operator Again Twice Thrice More"
   194  func getDisplayName(name string) string {
   195  	for _, sep := range ".-_ " {
   196  		splitName := strings.Split(name, string(sep))
   197  		for i := 0; i < len(splitName); i++ {
   198  			if splitName[i] == "" {
   199  				splitName = append(splitName[:i], splitName[i+1:]...)
   200  				i--
   201  			} else {
   202  				splitName[i] = strings.TrimSpace(splitName[i])
   203  			}
   204  		}
   205  		name = strings.Join(splitName, " ")
   206  	}
   207  	splitName := strings.Split(name, " ")
   208  	for i, word := range splitName {
   209  		temp := word
   210  		o := 0
   211  		for j, r := range word {
   212  			if unicode.IsUpper(r) {
   213  				if j > 0 && !unicode.IsUpper(rune(word[j-1])) {
   214  					temp = temp[0:j+o] + " " + temp[j+o:len(temp)]
   215  					o++
   216  				}
   217  			}
   218  		}
   219  		splitName[i] = temp
   220  	}
   221  	return strings.TrimSpace(strings.Title(strings.Join(splitName, " ")))
   222  }
   223  
   224  // initCSVFields initializes all csv fields that should be populated by a user
   225  // with sane defaults. initCSVFields should only be called for new csv's.
   226  func (s *CSV) initCSVFields(csv *olmapiv1alpha1.ClusterServiceVersion) {
   227  	// Metadata
   228  	csv.TypeMeta.APIVersion = olmapiv1alpha1.ClusterServiceVersionAPIVersion
   229  	csv.TypeMeta.Kind = olmapiv1alpha1.ClusterServiceVersionKind
   230  	csv.SetName(getCSVName(strings.ToLower(s.ProjectName), s.CSVVersion))
   231  	csv.SetNamespace("placeholder")
   232  	csv.SetAnnotations(map[string]string{"capabilities": "Basic Install"})
   233  
   234  	// Spec fields
   235  	csv.Spec.Version = *semver.New(s.CSVVersion)
   236  	csv.Spec.DisplayName = getDisplayName(s.ProjectName)
   237  	csv.Spec.Description = "Placeholder description"
   238  	csv.Spec.Maturity = "alpha"
   239  	csv.Spec.Provider = olmapiv1alpha1.AppLink{}
   240  	csv.Spec.Maintainers = make([]olmapiv1alpha1.Maintainer, 0)
   241  	csv.Spec.Links = make([]olmapiv1alpha1.AppLink, 0)
   242  }
   243  
   244  // setCSVDefaultFields sets default fields on older CSV versions or newly
   245  // initialized CSV's.
   246  func setCSVDefaultFields(csv *olmapiv1alpha1.ClusterServiceVersion) {
   247  	if len(csv.Spec.InstallModes) == 0 {
   248  		csv.Spec.InstallModes = []olmapiv1alpha1.InstallMode{
   249  			{Type: olmapiv1alpha1.InstallModeTypeOwnNamespace, Supported: true},
   250  			{Type: olmapiv1alpha1.InstallModeTypeSingleNamespace, Supported: true},
   251  			{Type: olmapiv1alpha1.InstallModeTypeMultiNamespace, Supported: false},
   252  			{Type: olmapiv1alpha1.InstallModeTypeAllNamespaces, Supported: true},
   253  		}
   254  	}
   255  }
   256  
   257  // TODO: validate that all fields from files are populated as expected
   258  // ex. add `resources` to a CRD
   259  
   260  func getEmptyRequiredCSVFields(csv *olmapiv1alpha1.ClusterServiceVersion) (fields []string) {
   261  	// Metadata
   262  	if csv.TypeMeta.APIVersion != olmapiv1alpha1.ClusterServiceVersionAPIVersion {
   263  		fields = append(fields, "apiVersion")
   264  	}
   265  	if csv.TypeMeta.Kind != olmapiv1alpha1.ClusterServiceVersionKind {
   266  		fields = append(fields, "kind")
   267  	}
   268  	if csv.ObjectMeta.Name == "" {
   269  		fields = append(fields, "metadata.name")
   270  	}
   271  	// Spec fields
   272  	if csv.Spec.Version.String() == "" {
   273  		fields = append(fields, "spec.version")
   274  	}
   275  	if csv.Spec.DisplayName == "" {
   276  		fields = append(fields, "spec.displayName")
   277  	}
   278  	if csv.Spec.Description == "" {
   279  		fields = append(fields, "spec.description")
   280  	}
   281  	if len(csv.Spec.Keywords) == 0 {
   282  		fields = append(fields, "spec.keywords")
   283  	}
   284  	if len(csv.Spec.Maintainers) == 0 {
   285  		fields = append(fields, "spec.maintainers")
   286  	}
   287  	if csv.Spec.Provider == (olmapiv1alpha1.AppLink{}) {
   288  		fields = append(fields, "spec.provider")
   289  	}
   290  	if csv.Spec.Maturity == "" {
   291  		fields = append(fields, "spec.maturity")
   292  	}
   293  
   294  	return fields
   295  }
   296  
   297  func joinFields(fields []string) string {
   298  	sb := &strings.Builder{}
   299  	for _, f := range fields {
   300  		sb.WriteString("\n\t" + f)
   301  	}
   302  	return sb.String()
   303  }
   304  
   305  // updateCSVVersions updates csv's version and data involving the version,
   306  // ex. ObjectMeta.Name, and place the old version in the `replaces` object,
   307  // if there is an old version to replace.
   308  func (s *CSV) updateCSVVersions(csv *olmapiv1alpha1.ClusterServiceVersion) error {
   309  
   310  	// Old csv version to replace, and updated csv version.
   311  	oldVer, newVer := csv.Spec.Version.String(), s.CSVVersion
   312  	if oldVer == newVer {
   313  		return nil
   314  	}
   315  
   316  	// We do not want to update versions in most fields, as these versions are
   317  	// independent of global csv version and will be updated elsewhere.
   318  	fieldsToUpdate := []interface{}{
   319  		&csv.ObjectMeta,
   320  		&csv.Spec.Labels,
   321  		&csv.Spec.Selector,
   322  	}
   323  	for _, v := range fieldsToUpdate {
   324  		err := replaceAllBytes(v, []byte(oldVer), []byte(newVer))
   325  		if err != nil {
   326  			return err
   327  		}
   328  	}
   329  
   330  	// Now replace all references to the old operator name.
   331  	lowerProjName := strings.ToLower(s.ProjectName)
   332  	oldCSVName := getCSVName(lowerProjName, oldVer)
   333  	newCSVName := getCSVName(lowerProjName, newVer)
   334  	err := replaceAllBytes(csv, []byte(oldCSVName), []byte(newCSVName))
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	csv.Spec.Version = *semver.New(newVer)
   340  	csv.Spec.Replaces = oldCSVName
   341  	return nil
   342  }
   343  
   344  func replaceAllBytes(v interface{}, old, new []byte) error {
   345  	b, err := json.Marshal(v)
   346  	if err != nil {
   347  		return err
   348  	}
   349  	b = bytes.Replace(b, old, new, -1)
   350  	if err = json.Unmarshal(b, v); err != nil {
   351  		return err
   352  	}
   353  	return nil
   354  }
   355  
   356  // updateCSVFromManifestFiles gathers relevant data from generated and
   357  // user-defined manifests and updates csv.
   358  func (s *CSV) updateCSVFromManifestFiles(cfg *CSVConfig, csv *olmapiv1alpha1.ClusterServiceVersion) error {
   359  	store := NewUpdaterStore()
   360  	otherSpecs := make(map[string][][]byte)
   361  	for _, f := range append(cfg.CRDCRPaths, cfg.OperatorPath, cfg.RolePath) {
   362  		yamlData, err := afero.ReadFile(s.getFS(), f)
   363  		if err != nil {
   364  			return err
   365  		}
   366  
   367  		scanner := yamlutil.NewYAMLScanner(yamlData)
   368  		for scanner.Scan() {
   369  			yamlSpec := scanner.Bytes()
   370  			kind, err := getKindfromYAML(yamlSpec)
   371  			if err != nil {
   372  				return err
   373  			}
   374  			found, err := store.AddToUpdater(yamlSpec, kind)
   375  			if err != nil {
   376  				return err
   377  			}
   378  			if !found {
   379  				if _, ok := otherSpecs[kind]; !ok {
   380  					otherSpecs[kind] = make([][]byte, 0)
   381  				}
   382  				otherSpecs[kind] = append(otherSpecs[kind], yamlSpec)
   383  			}
   384  		}
   385  		if err = scanner.Err(); err != nil {
   386  			return err
   387  		}
   388  	}
   389  
   390  	for k := range store.crds.crKinds {
   391  		if crSpecs, ok := otherSpecs[k]; ok {
   392  			for _, spec := range crSpecs {
   393  				if err := store.AddCR(spec); err != nil {
   394  					return err
   395  				}
   396  			}
   397  		}
   398  	}
   399  
   400  	return store.Apply(csv)
   401  }