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 }