github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/scaffold/crd.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 scaffold 16 17 import ( 18 "fmt" 19 "os" 20 "path/filepath" 21 "strings" 22 "sync" 23 24 "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input" 25 "github.com/operator-framework/operator-sdk/internal/util/k8sutil" 26 27 "github.com/ghodss/yaml" 28 "github.com/spf13/afero" 29 apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 crdgenerator "sigs.k8s.io/controller-tools/pkg/crd/generator" 32 ) 33 34 // CRD is the input needed to generate a deploy/crds/<group>_<version>_<kind>_crd.yaml file 35 type CRD struct { 36 input.Input 37 38 // Resource defines the inputs for the new custom resource definition 39 Resource *Resource 40 41 // IsOperatorGo is true when the operator is written in Go. 42 IsOperatorGo bool 43 44 once sync.Once 45 fs afero.Fs // For testing, ex. afero.NewMemMapFs() 46 } 47 48 func (s *CRD) initFS(fs afero.Fs) { 49 s.once.Do(func() { 50 s.fs = fs 51 }) 52 } 53 54 func (s *CRD) getFS() afero.Fs { 55 s.initFS(afero.NewOsFs()) 56 return s.fs 57 } 58 59 func (s *CRD) GetInput() (input.Input, error) { 60 if s.Path == "" { 61 fileName := fmt.Sprintf("%s_%s_%s_crd.yaml", 62 s.Resource.GoImportGroup, 63 strings.ToLower(s.Resource.Version), 64 s.Resource.LowerKind) 65 s.Path = filepath.Join(CRDsDir, fileName) 66 } 67 return s.Input, nil 68 } 69 70 var _ CustomRenderer = &CRD{} 71 72 func (s *CRD) SetFS(fs afero.Fs) { s.initFS(fs) } 73 74 func (s *CRD) CustomRender() ([]byte, error) { 75 i, err := s.GetInput() 76 if err != nil { 77 return nil, err 78 } 79 80 crd := &apiextv1beta1.CustomResourceDefinition{} 81 if s.IsOperatorGo { 82 // This sets domain as empty string when we can't extract it from FullGroup. 83 // In turn this defaults to extracting the domain from project root file 84 // in controller-tools. 85 fg := strings.SplitN(s.Resource.FullGroup, ".", 2) 86 domain := s.Resource.FullGroup 87 if len(fg) > 1 { 88 domain = fg[1] 89 } 90 fs := afero.NewMemMapFs() 91 g := &crdgenerator.Generator{ 92 RootPath: s.AbsProjectPath, 93 Domain: domain, 94 Repo: s.Repo, 95 OutputDir: ".", 96 SkipMapValidation: false, 97 OutFs: fs, 98 } 99 if err := g.ValidateAndInitFields(); err != nil { 100 return nil, err 101 } 102 if err := g.Do(); err != nil { 103 return nil, err 104 } 105 106 // controller-tools generates crd file names with no _crd.yaml suffix: 107 // <group>_<version>_<kind>.yaml. 108 path := strings.Replace(filepath.Base(i.Path), "_crd.yaml", ".yaml", 1) 109 b, err := afero.ReadFile(fs, path) 110 if err != nil { 111 if os.IsNotExist(err) { 112 return nil, fmt.Errorf("no API exists for Group %s Version %s Kind %s", 113 s.Resource.GoImportGroup, s.Resource.Version, s.Resource.Kind) 114 } 115 return nil, err 116 } 117 if err = yaml.Unmarshal(b, crd); err != nil { 118 return nil, err 119 } 120 // controller-tools does not set ListKind or Singular names. 121 setCRDNamesForResource(crd, s.Resource) 122 // Remove controller-tools default label. 123 delete(crd.Labels, "controller-tools.k8s.io") 124 } else { 125 // There are currently no commands to update CRD manifests for non-Go 126 // operators, so if a CRD manifests already exists for this gvk, this 127 // scaffold is a no-op. 128 path := filepath.Join(s.AbsProjectPath, i.Path) 129 if _, err = s.getFS().Stat(path); err == nil { 130 b, err := afero.ReadFile(s.getFS(), path) 131 if err != nil { 132 return nil, err 133 } 134 if len(b) == 0 { 135 crd = newCRDForResource(s.Resource) 136 } else { 137 if err = yaml.Unmarshal(b, crd); err != nil { 138 return nil, err 139 } 140 } 141 } 142 } 143 144 setCRDVersions(crd) 145 return k8sutil.GetObjectBytes(crd) 146 } 147 148 func newCRDForResource(r *Resource) *apiextv1beta1.CustomResourceDefinition { 149 crd := &apiextv1beta1.CustomResourceDefinition{ 150 TypeMeta: metav1.TypeMeta{ 151 APIVersion: apiextv1beta1.SchemeGroupVersion.String(), 152 Kind: "CustomResourceDefinition", 153 }, 154 ObjectMeta: metav1.ObjectMeta{ 155 Name: fmt.Sprintf("%s.%s", r.Resource, r.FullGroup), 156 }, 157 Spec: apiextv1beta1.CustomResourceDefinitionSpec{ 158 Group: r.FullGroup, 159 Scope: apiextv1beta1.NamespaceScoped, 160 Version: r.Version, 161 Subresources: &apiextv1beta1.CustomResourceSubresources{ 162 Status: &apiextv1beta1.CustomResourceSubresourceStatus{}, 163 }, 164 }, 165 } 166 setCRDNamesForResource(crd, r) 167 return crd 168 } 169 170 func setCRDNamesForResource(crd *apiextv1beta1.CustomResourceDefinition, r *Resource) { 171 if crd.Spec.Names.Kind == "" { 172 crd.Spec.Names.Kind = r.Kind 173 } 174 if crd.Spec.Names.ListKind == "" { 175 crd.Spec.Names.ListKind = r.Kind + "List" 176 } 177 if crd.Spec.Names.Plural == "" { 178 crd.Spec.Names.Plural = r.Resource 179 } 180 if crd.Spec.Names.Singular == "" { 181 crd.Spec.Names.Singular = r.LowerKind 182 } 183 } 184 185 func setCRDVersions(crd *apiextv1beta1.CustomResourceDefinition) { 186 // crd.Version is deprecated, use crd.Versions instead. 187 var crdVersions []apiextv1beta1.CustomResourceDefinitionVersion 188 if crd.Spec.Version != "" { 189 var verExists, hasStorageVer bool 190 for _, ver := range crd.Spec.Versions { 191 if crd.Spec.Version == ver.Name { 192 verExists = true 193 } 194 // There must be exactly one version flagged as a storage version. 195 if ver.Storage { 196 hasStorageVer = true 197 } 198 } 199 if !verExists { 200 crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{ 201 {Name: crd.Spec.Version, Served: true, Storage: !hasStorageVer}, 202 } 203 } 204 } else { 205 crdVersions = []apiextv1beta1.CustomResourceDefinitionVersion{ 206 {Name: "v1alpha1", Served: true, Storage: true}, 207 } 208 } 209 210 if len(crd.Spec.Versions) > 0 { 211 // crd.Version should always be the first element in crd.Versions. 212 crd.Spec.Versions = append(crdVersions, crd.Spec.Versions...) 213 } else { 214 crd.Spec.Versions = crdVersions 215 } 216 }